cli-claw-kit 0.0.1
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 +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/workbox-08d6266a.js +1 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
// Skills management routes
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
9
|
+
import { DATA_DIR } from '../config.js';
|
|
10
|
+
import { resolveAppPath } from '../app-root.js';
|
|
11
|
+
import { getSystemSettings, saveSystemSettings, } from '../runtime-config.js';
|
|
12
|
+
import { parseFrontmatter, validateSkillId, validateSkillPath, listFiles, scanSkillDirectory, } from '../skill-utils.js';
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
let skillInstallLock = Promise.resolve();
|
|
15
|
+
const skillsRoutes = new Hono();
|
|
16
|
+
// --- Utility Functions ---
|
|
17
|
+
function getUserSkillsDir(userId) {
|
|
18
|
+
return path.join(DATA_DIR, 'skills', userId);
|
|
19
|
+
}
|
|
20
|
+
function getGlobalSkillsDir() {
|
|
21
|
+
return path.join(os.homedir(), '.claude', 'skills');
|
|
22
|
+
}
|
|
23
|
+
function getProjectSkillsDir() {
|
|
24
|
+
return resolveAppPath('container', 'skills');
|
|
25
|
+
}
|
|
26
|
+
function getHostSyncManifestPath(userId) {
|
|
27
|
+
return path.join(DATA_DIR, 'skills', userId, '.host-sync.json');
|
|
28
|
+
}
|
|
29
|
+
function readHostSyncManifest(userId) {
|
|
30
|
+
try {
|
|
31
|
+
const data = fs.readFileSync(getHostSyncManifestPath(userId), 'utf-8');
|
|
32
|
+
return JSON.parse(data);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { syncedSkills: [], lastSyncAt: '' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeHostSyncManifest(userId, manifest) {
|
|
39
|
+
const manifestPath = getHostSyncManifestPath(userId);
|
|
40
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
41
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
42
|
+
}
|
|
43
|
+
function getSkillsManifestPath(userId) {
|
|
44
|
+
return path.join(DATA_DIR, 'skills', userId, '.skills-manifest.json');
|
|
45
|
+
}
|
|
46
|
+
function readSkillsManifest(userId) {
|
|
47
|
+
try {
|
|
48
|
+
const data = fs.readFileSync(getSkillsManifestPath(userId), 'utf-8');
|
|
49
|
+
return JSON.parse(data);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { skills: {} };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeSkillsManifest(userId, manifest) {
|
|
56
|
+
const manifestPath = getSkillsManifestPath(userId);
|
|
57
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
58
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Update the skills manifest after installing skills.
|
|
62
|
+
* Records packageName, installedAt, and source for each installed skill.
|
|
63
|
+
*/
|
|
64
|
+
function updateSkillsManifest(userId, packageName, installedSkillIds) {
|
|
65
|
+
const manifest = readSkillsManifest(userId);
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
for (const id of installedSkillIds) {
|
|
68
|
+
manifest.skills[id] = {
|
|
69
|
+
packageName,
|
|
70
|
+
installedAt: now,
|
|
71
|
+
source: 'skills.sh',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
writeSkillsManifest(userId, manifest);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove a skill from the manifest when it is deleted.
|
|
78
|
+
*/
|
|
79
|
+
function removeFromSkillsManifest(userId, skillId) {
|
|
80
|
+
const manifest = readSkillsManifest(userId);
|
|
81
|
+
if (skillId in manifest.skills) {
|
|
82
|
+
delete manifest.skills[skillId];
|
|
83
|
+
writeSkillsManifest(userId, manifest);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// validateSkillId, validateSkillPath, parseFrontmatter, listFiles, scanSkillDirectory
|
|
87
|
+
// are imported from '../skill-utils.js'
|
|
88
|
+
function scanDirectory(rootDir, source) {
|
|
89
|
+
return scanSkillDirectory(rootDir, source);
|
|
90
|
+
}
|
|
91
|
+
function discoverSkills(userId) {
|
|
92
|
+
const userSkills = scanDirectory(getUserSkillsDir(userId), 'user');
|
|
93
|
+
const projectSkills = scanDirectory(getProjectSkillsDir(), 'project');
|
|
94
|
+
// 读取 host sync manifest 标记同步来源
|
|
95
|
+
const hostManifest = readHostSyncManifest(userId);
|
|
96
|
+
const syncedSet = new Set(hostManifest.syncedSkills);
|
|
97
|
+
// 读取 skills manifest 补充安装元数据
|
|
98
|
+
const skillsManifest = readSkillsManifest(userId);
|
|
99
|
+
for (const skill of userSkills) {
|
|
100
|
+
if (syncedSet.has(skill.id)) {
|
|
101
|
+
skill.syncedFromHost = true;
|
|
102
|
+
}
|
|
103
|
+
const meta = skillsManifest.skills[skill.id];
|
|
104
|
+
if (meta) {
|
|
105
|
+
skill.packageName = meta.packageName;
|
|
106
|
+
skill.installedAt = meta.installedAt;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [...userSkills, ...projectSkills];
|
|
110
|
+
}
|
|
111
|
+
function getSkillDetail(skillId, userId) {
|
|
112
|
+
if (!validateSkillId(skillId))
|
|
113
|
+
return null;
|
|
114
|
+
const searchDirs = [
|
|
115
|
+
{ rootDir: getUserSkillsDir(userId), source: 'user' },
|
|
116
|
+
{ rootDir: getProjectSkillsDir(), source: 'project' },
|
|
117
|
+
];
|
|
118
|
+
const hostManifest = readHostSyncManifest(userId);
|
|
119
|
+
const syncedSet = new Set(hostManifest.syncedSkills);
|
|
120
|
+
const skillsManifest = readSkillsManifest(userId);
|
|
121
|
+
for (const { rootDir, source } of searchDirs) {
|
|
122
|
+
const skillDir = path.join(rootDir, skillId);
|
|
123
|
+
if (!fs.existsSync(skillDir))
|
|
124
|
+
continue;
|
|
125
|
+
if (!validateSkillPath(rootDir, skillDir))
|
|
126
|
+
continue;
|
|
127
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
128
|
+
const skillMdDisabledPath = path.join(skillDir, 'SKILL.md.disabled');
|
|
129
|
+
let enabled = false;
|
|
130
|
+
let skillFilePath = null;
|
|
131
|
+
if (fs.existsSync(skillMdPath)) {
|
|
132
|
+
enabled = true;
|
|
133
|
+
skillFilePath = skillMdPath;
|
|
134
|
+
}
|
|
135
|
+
else if (fs.existsSync(skillMdDisabledPath)) {
|
|
136
|
+
enabled = false;
|
|
137
|
+
skillFilePath = skillMdDisabledPath;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const content = fs.readFileSync(skillFilePath, 'utf-8');
|
|
144
|
+
const frontmatter = parseFrontmatter(content);
|
|
145
|
+
const stats = fs.statSync(skillDir);
|
|
146
|
+
const detail = {
|
|
147
|
+
id: skillId,
|
|
148
|
+
name: frontmatter.name || skillId,
|
|
149
|
+
description: frontmatter.description || '',
|
|
150
|
+
source,
|
|
151
|
+
enabled,
|
|
152
|
+
userInvocable: frontmatter['user-invocable'] === undefined
|
|
153
|
+
? true
|
|
154
|
+
: frontmatter['user-invocable'] !== 'false',
|
|
155
|
+
allowedTools: frontmatter['allowed-tools']
|
|
156
|
+
? frontmatter['allowed-tools'].split(',').map((t) => t.trim())
|
|
157
|
+
: [],
|
|
158
|
+
argumentHint: frontmatter['argument-hint'] || null,
|
|
159
|
+
updatedAt: stats.mtime.toISOString(),
|
|
160
|
+
files: listFiles(skillDir),
|
|
161
|
+
content,
|
|
162
|
+
};
|
|
163
|
+
if (source === 'user') {
|
|
164
|
+
if (syncedSet.has(skillId)) {
|
|
165
|
+
detail.syncedFromHost = true;
|
|
166
|
+
}
|
|
167
|
+
const meta = skillsManifest.skills[skillId];
|
|
168
|
+
if (meta) {
|
|
169
|
+
detail.packageName = meta.packageName;
|
|
170
|
+
detail.installedAt = meta.installedAt;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return detail;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Skip malformed skill
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Parse the output of `npx skills find <query>` to extract search results.
|
|
183
|
+
* The output contains ANSI codes and formatted text like:
|
|
184
|
+
* owner/repo@skill-name
|
|
185
|
+
* https://skills.sh/owner/repo/skill
|
|
186
|
+
*/
|
|
187
|
+
function parseSearchOutput(output) {
|
|
188
|
+
// Strip ANSI escape codes
|
|
189
|
+
const clean = output.replace(/\x1b\[[0-9;]*m/g, '');
|
|
190
|
+
const results = [];
|
|
191
|
+
const lines = clean
|
|
192
|
+
.split('\n')
|
|
193
|
+
.map((l) => l.trim())
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
for (let i = 0; i < lines.length; i++) {
|
|
196
|
+
const line = lines[i];
|
|
197
|
+
// Match package pattern: owner/repo or owner/repo@skill
|
|
198
|
+
const pkgMatch = line.match(/^([\w\-]+\/[\w\-.]+(?:@[\w\-.]+)?)$/);
|
|
199
|
+
if (pkgMatch) {
|
|
200
|
+
const pkg = pkgMatch[1];
|
|
201
|
+
// Next line might be the URL (possibly prefixed with └ or similar chars)
|
|
202
|
+
let url = '';
|
|
203
|
+
if (i + 1 < lines.length) {
|
|
204
|
+
const nextLine = lines[i + 1].replace(/^[└├│─\s]+/, '');
|
|
205
|
+
if (nextLine.startsWith('http')) {
|
|
206
|
+
url = nextLine;
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
results.push({ package: pkg, url });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return results;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Find skill entries under a path that were modified after the given timestamp.
|
|
217
|
+
* Handles both real directories and symlinks (skills CLI creates symlinks in
|
|
218
|
+
* ~/.claude/skills/ pointing to ~/.agents/skills/).
|
|
219
|
+
* Returns entry names.
|
|
220
|
+
*/
|
|
221
|
+
function findModifiedEntries(dir, afterMs) {
|
|
222
|
+
const result = [];
|
|
223
|
+
if (!fs.existsSync(dir))
|
|
224
|
+
return result;
|
|
225
|
+
try {
|
|
226
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const fullPath = path.join(dir, entry.name);
|
|
229
|
+
try {
|
|
230
|
+
// Use lstat for symlinks, stat (follows symlink) for mtime of real target
|
|
231
|
+
const lstat = fs.lstatSync(fullPath);
|
|
232
|
+
if (lstat.isSymbolicLink()) {
|
|
233
|
+
// Symlink: check both the symlink creation time and target mtime
|
|
234
|
+
if (lstat.mtimeMs >= afterMs) {
|
|
235
|
+
result.push(entry.name);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Also check the resolved target's mtime
|
|
239
|
+
const realStat = fs.statSync(fullPath);
|
|
240
|
+
if (realStat.mtimeMs >= afterMs) {
|
|
241
|
+
result.push(entry.name);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (lstat.isDirectory()) {
|
|
245
|
+
if (lstat.mtimeMs >= afterMs) {
|
|
246
|
+
result.push(entry.name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// skip broken symlinks etc.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// ignore
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Copy a skill entry (directory or symlink target) to dest.
|
|
262
|
+
* Resolves symlinks and copies the real content so the copy is self-contained.
|
|
263
|
+
*/
|
|
264
|
+
function copySkillToUser(src, dest) {
|
|
265
|
+
// Resolve symlink to get the real directory
|
|
266
|
+
let realSrc = src;
|
|
267
|
+
try {
|
|
268
|
+
const lstat = fs.lstatSync(src);
|
|
269
|
+
if (lstat.isSymbolicLink()) {
|
|
270
|
+
realSrc = fs.realpathSync(src);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// use src as-is
|
|
275
|
+
}
|
|
276
|
+
fs.cpSync(realSrc, dest, { recursive: true });
|
|
277
|
+
}
|
|
278
|
+
const SEARCH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
279
|
+
const SEARCH_CACHE_MAX = 100;
|
|
280
|
+
const searchCache = new Map();
|
|
281
|
+
function getCachedSearch(key) {
|
|
282
|
+
const entry = searchCache.get(key);
|
|
283
|
+
if (!entry)
|
|
284
|
+
return null;
|
|
285
|
+
if (Date.now() > entry.expiresAt) {
|
|
286
|
+
searchCache.delete(key);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return entry.value;
|
|
290
|
+
}
|
|
291
|
+
function setCachedSearch(key, value) {
|
|
292
|
+
// Evict oldest if at capacity
|
|
293
|
+
if (searchCache.size >= SEARCH_CACHE_MAX) {
|
|
294
|
+
const oldest = searchCache.keys().next().value;
|
|
295
|
+
if (oldest !== undefined)
|
|
296
|
+
searchCache.delete(oldest);
|
|
297
|
+
}
|
|
298
|
+
searchCache.set(key, { value, expiresAt: Date.now() + SEARCH_CACHE_TTL });
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Search skills via skills.sh API.
|
|
302
|
+
* Returns structured results with install counts.
|
|
303
|
+
*/
|
|
304
|
+
async function searchSkillsApi(query) {
|
|
305
|
+
const cached = getCachedSearch(query);
|
|
306
|
+
if (cached)
|
|
307
|
+
return cached;
|
|
308
|
+
try {
|
|
309
|
+
const resp = await fetch(`https://skills.sh/api/search?q=${encodeURIComponent(query)}&limit=20`, { signal: AbortSignal.timeout(10_000) });
|
|
310
|
+
if (!resp.ok)
|
|
311
|
+
throw new Error(`skills.sh returned ${resp.status}`);
|
|
312
|
+
const data = (await resp.json());
|
|
313
|
+
const results = (data.skills || []).map((s) => ({
|
|
314
|
+
package: s.source === s.skillId || !s.skillId
|
|
315
|
+
? s.source
|
|
316
|
+
: `${s.source}@${s.skillId}`,
|
|
317
|
+
url: `https://skills.sh/s/${s.id}`,
|
|
318
|
+
description: '',
|
|
319
|
+
installs: s.installs,
|
|
320
|
+
skillId: s.skillId,
|
|
321
|
+
source: s.source,
|
|
322
|
+
}));
|
|
323
|
+
setCachedSearch(query, results);
|
|
324
|
+
return results;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Fallback to npx skills find
|
|
328
|
+
return searchSkillsFallback(query);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Fallback search using npx skills find CLI.
|
|
333
|
+
*/
|
|
334
|
+
async function searchSkillsFallback(query) {
|
|
335
|
+
try {
|
|
336
|
+
const { stdout } = await execFileAsync('npx', ['-y', 'skills', 'find', query], { timeout: 30_000 });
|
|
337
|
+
return parseSearchOutput(stdout);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (error && typeof error === 'object' && 'stdout' in error) {
|
|
341
|
+
const results = parseSearchOutput(error.stdout || '');
|
|
342
|
+
if (results.length > 0)
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Fetch SKILL.md content from GitHub for a given source repo and skill ID.
|
|
350
|
+
* Tries multiple common directory layouts.
|
|
351
|
+
*/
|
|
352
|
+
async function fetchSkillMdFromGitHub(source, skillId) {
|
|
353
|
+
// Try common paths where SKILL.md might live
|
|
354
|
+
const pathCandidates = [
|
|
355
|
+
`skills/${skillId}/SKILL.md`,
|
|
356
|
+
`${skillId}/SKILL.md`,
|
|
357
|
+
`.claude/skills/${skillId}/SKILL.md`,
|
|
358
|
+
`SKILL.md`,
|
|
359
|
+
];
|
|
360
|
+
for (const branch of ['main', 'master']) {
|
|
361
|
+
for (const filePath of pathCandidates) {
|
|
362
|
+
try {
|
|
363
|
+
const url = `https://raw.githubusercontent.com/${source}/${branch}/${filePath}`;
|
|
364
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(8_000) });
|
|
365
|
+
if (!resp.ok)
|
|
366
|
+
continue;
|
|
367
|
+
const content = await resp.text();
|
|
368
|
+
// Verify it looks like a SKILL.md (has frontmatter)
|
|
369
|
+
if (!content.startsWith('---'))
|
|
370
|
+
continue;
|
|
371
|
+
const frontmatter = parseFrontmatter(content);
|
|
372
|
+
return {
|
|
373
|
+
content,
|
|
374
|
+
description: frontmatter.description || '',
|
|
375
|
+
skillName: frontmatter.name || skillId,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
async function withSkillInstallLock(fn) {
|
|
386
|
+
const previous = skillInstallLock.catch(() => undefined);
|
|
387
|
+
let release = () => undefined;
|
|
388
|
+
const current = new Promise((resolve) => {
|
|
389
|
+
release = resolve;
|
|
390
|
+
});
|
|
391
|
+
skillInstallLock = previous.then(() => current);
|
|
392
|
+
await previous;
|
|
393
|
+
try {
|
|
394
|
+
return await fn();
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
release();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// --- Routes ---
|
|
401
|
+
skillsRoutes.get('/', authMiddleware, (c) => {
|
|
402
|
+
const authUser = c.get('user');
|
|
403
|
+
const skills = discoverSkills(authUser.id);
|
|
404
|
+
return c.json({ skills });
|
|
405
|
+
});
|
|
406
|
+
skillsRoutes.get('/search', authMiddleware, async (c) => {
|
|
407
|
+
const query = c.req.query('q')?.trim();
|
|
408
|
+
if (!query) {
|
|
409
|
+
return c.json({ results: [] });
|
|
410
|
+
}
|
|
411
|
+
const results = await searchSkillsApi(query);
|
|
412
|
+
return c.json({ results });
|
|
413
|
+
});
|
|
414
|
+
skillsRoutes.get('/search/detail', authMiddleware, async (c) => {
|
|
415
|
+
const source = c.req.query('source')?.trim();
|
|
416
|
+
const skillId = c.req.query('skillId')?.trim();
|
|
417
|
+
// Support legacy url-based lookup for backwards compat
|
|
418
|
+
const url = c.req.query('url')?.trim();
|
|
419
|
+
if (source && skillId) {
|
|
420
|
+
// New path: fetch SKILL.md from GitHub using source/skillId
|
|
421
|
+
const result = await fetchSkillMdFromGitHub(source, skillId);
|
|
422
|
+
if (!result) {
|
|
423
|
+
return c.json({ detail: null });
|
|
424
|
+
}
|
|
425
|
+
return c.json({
|
|
426
|
+
detail: {
|
|
427
|
+
description: result.description,
|
|
428
|
+
skillName: result.skillName,
|
|
429
|
+
readme: result.content,
|
|
430
|
+
installs: '',
|
|
431
|
+
age: '',
|
|
432
|
+
features: [],
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// Legacy: extract source/skillId from skills.sh URL
|
|
437
|
+
if (url) {
|
|
438
|
+
try {
|
|
439
|
+
const parsed = new URL(url);
|
|
440
|
+
if (parsed.hostname === 'skills.sh') {
|
|
441
|
+
// URL pattern: https://skills.sh/s/{owner}/{repo}/{skillId}
|
|
442
|
+
const segments = parsed.pathname
|
|
443
|
+
.replace(/^\/s\//, '')
|
|
444
|
+
.split('/')
|
|
445
|
+
.filter(Boolean);
|
|
446
|
+
if (segments.length >= 3) {
|
|
447
|
+
const srcFromUrl = `${segments[0]}/${segments[1]}`;
|
|
448
|
+
const skillIdFromUrl = segments[2];
|
|
449
|
+
const result = await fetchSkillMdFromGitHub(srcFromUrl, skillIdFromUrl);
|
|
450
|
+
if (result) {
|
|
451
|
+
return c.json({
|
|
452
|
+
detail: {
|
|
453
|
+
description: result.description,
|
|
454
|
+
skillName: result.skillName,
|
|
455
|
+
readme: result.content,
|
|
456
|
+
installs: '',
|
|
457
|
+
age: '',
|
|
458
|
+
features: [],
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// fall through
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return c.json({ detail: null });
|
|
470
|
+
});
|
|
471
|
+
// Get sync status (last sync time + auto-sync config)
|
|
472
|
+
skillsRoutes.get('/sync-status', authMiddleware, (c) => {
|
|
473
|
+
const authUser = c.get('user');
|
|
474
|
+
const manifest = readHostSyncManifest(authUser.id);
|
|
475
|
+
const settings = getSystemSettings();
|
|
476
|
+
return c.json({
|
|
477
|
+
lastSyncAt: manifest.lastSyncAt || null,
|
|
478
|
+
syncedCount: manifest.syncedSkills.length,
|
|
479
|
+
autoSyncEnabled: settings.skillAutoSyncEnabled,
|
|
480
|
+
autoSyncIntervalMinutes: settings.skillAutoSyncIntervalMinutes,
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
// Toggle auto-sync on/off (admin only)
|
|
484
|
+
skillsRoutes.put('/sync-settings', authMiddleware, async (c) => {
|
|
485
|
+
const authUser = c.get('user');
|
|
486
|
+
if (authUser.role !== 'admin') {
|
|
487
|
+
return c.json({ error: 'Only admin can change sync settings' }, 403);
|
|
488
|
+
}
|
|
489
|
+
const body = await c.req.json().catch(() => ({}));
|
|
490
|
+
const updates = {};
|
|
491
|
+
if (typeof body.autoSyncEnabled === 'boolean') {
|
|
492
|
+
updates.skillAutoSyncEnabled = body.autoSyncEnabled;
|
|
493
|
+
}
|
|
494
|
+
if (typeof body.autoSyncIntervalMinutes === 'number' &&
|
|
495
|
+
body.autoSyncIntervalMinutes >= 1) {
|
|
496
|
+
updates.skillAutoSyncIntervalMinutes = body.autoSyncIntervalMinutes;
|
|
497
|
+
}
|
|
498
|
+
const saved = saveSystemSettings(updates);
|
|
499
|
+
return c.json({
|
|
500
|
+
autoSyncEnabled: saved.skillAutoSyncEnabled,
|
|
501
|
+
autoSyncIntervalMinutes: saved.skillAutoSyncIntervalMinutes,
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
skillsRoutes.get('/:id', authMiddleware, (c) => {
|
|
505
|
+
const id = c.req.param('id');
|
|
506
|
+
const authUser = c.get('user');
|
|
507
|
+
const skill = getSkillDetail(id, authUser.id);
|
|
508
|
+
if (!skill) {
|
|
509
|
+
return c.json({ error: 'Skill not found' }, 404);
|
|
510
|
+
}
|
|
511
|
+
return c.json({ skill });
|
|
512
|
+
});
|
|
513
|
+
// Toggle enable/disable for user-level skills via SKILL.md ↔ SKILL.md.disabled rename.
|
|
514
|
+
// Project-level skills are read-only.
|
|
515
|
+
skillsRoutes.patch('/:id', authMiddleware, async (c) => {
|
|
516
|
+
const id = c.req.param('id');
|
|
517
|
+
const authUser = c.get('user');
|
|
518
|
+
const { enabled } = await c.req.json();
|
|
519
|
+
if (!validateSkillId(id))
|
|
520
|
+
return c.json({ error: 'Invalid skill ID' }, 400);
|
|
521
|
+
const userDir = getUserSkillsDir(authUser.id);
|
|
522
|
+
const skillDir = path.join(userDir, id);
|
|
523
|
+
if (!fs.existsSync(skillDir)) {
|
|
524
|
+
return c.json({ error: 'Skill not found or is not a user-level skill' }, 404);
|
|
525
|
+
}
|
|
526
|
+
if (!validateSkillPath(userDir, skillDir)) {
|
|
527
|
+
return c.json({ error: 'Invalid skill path' }, 400);
|
|
528
|
+
}
|
|
529
|
+
const srcPath = path.join(skillDir, enabled ? 'SKILL.md.disabled' : 'SKILL.md');
|
|
530
|
+
const dstPath = path.join(skillDir, enabled ? 'SKILL.md' : 'SKILL.md.disabled');
|
|
531
|
+
if (!fs.existsSync(srcPath)) {
|
|
532
|
+
return c.json({ error: 'Skill not found or already in desired state' }, 404);
|
|
533
|
+
}
|
|
534
|
+
fs.renameSync(srcPath, dstPath);
|
|
535
|
+
return c.json({ success: true });
|
|
536
|
+
});
|
|
537
|
+
/**
|
|
538
|
+
* Delete a user-level skill by ID.
|
|
539
|
+
* Reusable by both the HTTP route and IPC handler.
|
|
540
|
+
*/
|
|
541
|
+
function deleteSkillForUser(userId, skillId) {
|
|
542
|
+
if (!validateSkillId(skillId)) {
|
|
543
|
+
return { success: false, error: 'Invalid skill ID' };
|
|
544
|
+
}
|
|
545
|
+
const userDir = getUserSkillsDir(userId);
|
|
546
|
+
const skillDir = path.join(userDir, skillId);
|
|
547
|
+
if (!fs.existsSync(skillDir)) {
|
|
548
|
+
return {
|
|
549
|
+
success: false,
|
|
550
|
+
error: 'Skill not found or is a project-level skill',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (!validateSkillPath(userDir, skillDir)) {
|
|
554
|
+
return { success: false, error: 'Invalid skill path' };
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
558
|
+
removeFromSkillsManifest(userId, skillId);
|
|
559
|
+
return { success: true };
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
return {
|
|
563
|
+
success: false,
|
|
564
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
skillsRoutes.delete('/:id', authMiddleware, async (c) => {
|
|
569
|
+
const id = c.req.param('id');
|
|
570
|
+
const authUser = c.get('user');
|
|
571
|
+
const result = deleteSkillForUser(authUser.id, id);
|
|
572
|
+
if (!result.success) {
|
|
573
|
+
const status = result.error === 'Invalid skill ID' ||
|
|
574
|
+
result.error === 'Invalid skill path'
|
|
575
|
+
? 400
|
|
576
|
+
: result.error?.includes('not found')
|
|
577
|
+
? 404
|
|
578
|
+
: 500;
|
|
579
|
+
return c.json({ error: result.error }, status);
|
|
580
|
+
}
|
|
581
|
+
return c.json({ success: true });
|
|
582
|
+
});
|
|
583
|
+
/**
|
|
584
|
+
* Install a skill package for a specific user.
|
|
585
|
+
* Uses a temporary HOME directory to isolate `npx skills add --global` from
|
|
586
|
+
* the real ~/.claude/skills, eliminating race conditions across concurrent installs.
|
|
587
|
+
* Reusable by both the HTTP route and IPC handler.
|
|
588
|
+
*/
|
|
589
|
+
async function installSkillForUser(userId, pkg) {
|
|
590
|
+
if (!/^[\w\-]+\/[\w\-.]+(?:[@#][\w\-.\/]+)?$/.test(pkg) &&
|
|
591
|
+
!/^https?:\/\//.test(pkg)) {
|
|
592
|
+
return { success: false, error: 'Invalid package name format' };
|
|
593
|
+
}
|
|
594
|
+
// Create an isolated temp directory to act as HOME so `--global` installs
|
|
595
|
+
// into tempHome/.claude/skills/ instead of the real ~/.claude/skills/.
|
|
596
|
+
// This avoids any race condition when multiple installs run concurrently.
|
|
597
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-install-'));
|
|
598
|
+
const tempSkillsDir = path.join(tempHome, '.claude', 'skills');
|
|
599
|
+
fs.mkdirSync(tempSkillsDir, { recursive: true });
|
|
600
|
+
try {
|
|
601
|
+
await execFileAsync('npx', ['-y', 'skills', 'add', pkg, '--global', '--yes', '-a', 'claude-code'], {
|
|
602
|
+
timeout: 60_000,
|
|
603
|
+
env: { ...process.env, HOME: tempHome },
|
|
604
|
+
});
|
|
605
|
+
// Discover all skill directories installed into the temp location
|
|
606
|
+
const installedEntries = [];
|
|
607
|
+
if (fs.existsSync(tempSkillsDir)) {
|
|
608
|
+
for (const entry of fs.readdirSync(tempSkillsDir, {
|
|
609
|
+
withFileTypes: true,
|
|
610
|
+
})) {
|
|
611
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
612
|
+
installedEntries.push(entry.name);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (installedEntries.length === 0) {
|
|
617
|
+
return {
|
|
618
|
+
success: false,
|
|
619
|
+
error: 'No skills were installed — package may be invalid',
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
// Copy resolved skill content to per-user directory
|
|
623
|
+
const userDir = getUserSkillsDir(userId);
|
|
624
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
625
|
+
for (const name of installedEntries) {
|
|
626
|
+
const src = path.join(tempSkillsDir, name);
|
|
627
|
+
const dest = path.join(userDir, name);
|
|
628
|
+
if (fs.existsSync(dest)) {
|
|
629
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
630
|
+
}
|
|
631
|
+
copySkillToUser(src, dest);
|
|
632
|
+
}
|
|
633
|
+
// Write manifest metadata
|
|
634
|
+
updateSkillsManifest(userId, pkg, installedEntries);
|
|
635
|
+
return { success: true, installed: installedEntries };
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
// Always clean up the temp directory
|
|
645
|
+
try {
|
|
646
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
/* ignore cleanup errors */
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Sync host-level skills (~/.claude/skills/) to a user's directory.
|
|
655
|
+
* Standalone function usable from both the API route and the auto-sync timer.
|
|
656
|
+
*/
|
|
657
|
+
async function syncHostSkillsForUser(userId) {
|
|
658
|
+
return withSkillInstallLock(async () => {
|
|
659
|
+
const hostDir = getGlobalSkillsDir();
|
|
660
|
+
const userDir = getUserSkillsDir(userId);
|
|
661
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
662
|
+
// 1. 扫描宿主机 skills
|
|
663
|
+
const hostSkillNames = [];
|
|
664
|
+
if (fs.existsSync(hostDir)) {
|
|
665
|
+
for (const entry of fs.readdirSync(hostDir, { withFileTypes: true })) {
|
|
666
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
667
|
+
continue;
|
|
668
|
+
const skillDir = path.join(hostDir, entry.name);
|
|
669
|
+
try {
|
|
670
|
+
const realPath = fs.realpathSync(skillDir);
|
|
671
|
+
if (fs.existsSync(path.join(realPath, 'SKILL.md')) ||
|
|
672
|
+
fs.existsSync(path.join(realPath, 'SKILL.md.disabled'))) {
|
|
673
|
+
hostSkillNames.push(entry.name);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// 跳过 broken symlinks
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// 2. 读取 manifest
|
|
682
|
+
const manifest = readHostSyncManifest(userId);
|
|
683
|
+
const previouslySynced = new Set(manifest.syncedSkills);
|
|
684
|
+
// 3. 检测用户目录中手动安装的 skills
|
|
685
|
+
const existingUserSkills = new Set();
|
|
686
|
+
if (fs.existsSync(userDir)) {
|
|
687
|
+
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
|
|
688
|
+
if (entry.isDirectory())
|
|
689
|
+
existingUserSkills.add(entry.name);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const stats = { added: 0, updated: 0, deleted: 0, skipped: 0 };
|
|
693
|
+
const newSyncedList = [];
|
|
694
|
+
// 4. 同步:新增/更新
|
|
695
|
+
for (const name of hostSkillNames) {
|
|
696
|
+
const isManuallyInstalled = existingUserSkills.has(name) && !previouslySynced.has(name);
|
|
697
|
+
if (isManuallyInstalled) {
|
|
698
|
+
stats.skipped++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const src = path.join(hostDir, name);
|
|
702
|
+
const dest = path.join(userDir, name);
|
|
703
|
+
if (existingUserSkills.has(name)) {
|
|
704
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
705
|
+
copySkillToUser(src, dest);
|
|
706
|
+
stats.updated++;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
copySkillToUser(src, dest);
|
|
710
|
+
stats.added++;
|
|
711
|
+
}
|
|
712
|
+
newSyncedList.push(name);
|
|
713
|
+
}
|
|
714
|
+
// 5. 删除宿主机已移除的(仅清理之前同步来的)
|
|
715
|
+
const hostSkillSet = new Set(hostSkillNames);
|
|
716
|
+
for (const name of previouslySynced) {
|
|
717
|
+
if (!hostSkillSet.has(name) && existingUserSkills.has(name)) {
|
|
718
|
+
const dest = path.join(userDir, name);
|
|
719
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
720
|
+
stats.deleted++;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// 6. 更新 manifest
|
|
724
|
+
writeHostSyncManifest(userId, {
|
|
725
|
+
syncedSkills: newSyncedList,
|
|
726
|
+
lastSyncAt: new Date().toISOString(),
|
|
727
|
+
});
|
|
728
|
+
return { stats, total: hostSkillNames.length };
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
// Sync host-level skills — API endpoint (admin only).
|
|
732
|
+
skillsRoutes.post('/sync-host', authMiddleware, async (c) => {
|
|
733
|
+
const authUser = c.get('user');
|
|
734
|
+
if (authUser.role !== 'admin') {
|
|
735
|
+
return c.json({ error: 'Only admin can sync host skills' }, 403);
|
|
736
|
+
}
|
|
737
|
+
const result = await syncHostSkillsForUser(authUser.id);
|
|
738
|
+
return c.json(result);
|
|
739
|
+
});
|
|
740
|
+
skillsRoutes.post('/install', authMiddleware, async (c) => {
|
|
741
|
+
const authUser = c.get('user');
|
|
742
|
+
const body = await c.req.json().catch(() => ({}));
|
|
743
|
+
if (typeof body.package !== 'string') {
|
|
744
|
+
return c.json({ error: 'package field must be string' }, 400);
|
|
745
|
+
}
|
|
746
|
+
const pkg = body.package.trim();
|
|
747
|
+
const result = await installSkillForUser(authUser.id, pkg);
|
|
748
|
+
if (!result.success) {
|
|
749
|
+
return c.json({ error: 'Failed to install skill', details: result.error }, result.error === 'Invalid package name format' ? 400 : 500);
|
|
750
|
+
}
|
|
751
|
+
return c.json({ success: true, installed: result.installed });
|
|
752
|
+
});
|
|
753
|
+
// Reinstall a skill by its ID — requires the skill to have a packageName in the manifest.
|
|
754
|
+
skillsRoutes.post('/:id/reinstall', authMiddleware, async (c) => {
|
|
755
|
+
const id = c.req.param('id');
|
|
756
|
+
const authUser = c.get('user');
|
|
757
|
+
if (!validateSkillId(id)) {
|
|
758
|
+
return c.json({ error: 'Invalid skill ID' }, 400);
|
|
759
|
+
}
|
|
760
|
+
const manifest = readSkillsManifest(authUser.id);
|
|
761
|
+
const meta = manifest.skills[id];
|
|
762
|
+
if (!meta?.packageName) {
|
|
763
|
+
return c.json({ error: 'Skill has no package info — cannot reinstall' }, 400);
|
|
764
|
+
}
|
|
765
|
+
// Delete then reinstall
|
|
766
|
+
const deleteResult = deleteSkillForUser(authUser.id, id);
|
|
767
|
+
if (!deleteResult.success) {
|
|
768
|
+
return c.json({ error: 'Failed to delete old skill', details: deleteResult.error }, 500);
|
|
769
|
+
}
|
|
770
|
+
const installResult = await installSkillForUser(authUser.id, meta.packageName);
|
|
771
|
+
if (!installResult.success) {
|
|
772
|
+
return c.json({ error: 'Failed to reinstall skill', details: installResult.error }, 500);
|
|
773
|
+
}
|
|
774
|
+
return c.json({ success: true, installed: installResult.installed });
|
|
775
|
+
});
|
|
776
|
+
export { getUserSkillsDir, installSkillForUser, deleteSkillForUser, syncHostSkillsForUser, };
|
|
777
|
+
export default skillsRoutes;
|