dsh-desktop-kit 0.0.0-stage → 2.0.0
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 +152 -2
- package/client.js +4101 -0
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/COMPATIBILITY.md +61 -0
- package/docs/INSTALL.md +84 -0
- package/docs/PRIVACY.md +50 -0
- package/docs/ROADMAP.md +57 -0
- package/docs/TOKENS.md +118 -0
- package/docs/TROUBLESHOOTING.md +68 -0
- package/docs/images/hero.png +0 -0
- package/index.js +84 -0
- package/package.json +70 -4
- package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
- package/plugins/dsh-gkb-ru-ui/lib/client.js +1331 -0
- package/plugins/dsh-gkb-ru-ui/lib/index.js +186 -0
- package/plugins/dsh-gkb-ru-ui/lib/skills.js +570 -0
- package/plugins/dsh-gkb-ru-ui/package.json +28 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
- package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/README.md +70 -0
- package/plugins/dsh-peak-guard/client.js +741 -0
- package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
- package/plugins/dsh-peak-guard/icon.svg +12 -0
- package/plugins/dsh-peak-guard/index.js +2 -0
- package/plugins/dsh-peak-guard/locale/en.json +6 -0
- package/plugins/dsh-peak-guard/locale/ru.json +6 -0
- package/plugins/dsh-peak-guard/locale/zh.json +6 -0
- package/plugins/dsh-peak-guard/package.json +33 -0
- package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
- package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
- package/plugins/dsh-profile-transfer/README.md +62 -0
- package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
- package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
- package/plugins/dsh-profile-transfer/bootstrap/README-/320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/232/320/220.md +42 -0
- package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
- package/plugins/dsh-profile-transfer/bootstrap//320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/230/320/242/320/254.cmd +5 -0
- package/plugins/dsh-profile-transfer/client.js +806 -0
- package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
- package/plugins/dsh-profile-transfer/icon.svg +13 -0
- package/plugins/dsh-profile-transfer/index.js +375 -0
- package/plugins/dsh-profile-transfer/locale/en.json +6 -0
- package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
- package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
- package/plugins/dsh-profile-transfer/package.json +36 -0
- package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
- package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
- package/plugins/dsh-profile-transfer/transfer.js +1004 -0
- package/plugins/dsh-project-memory/README.md +71 -0
- package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
- package/plugins/dsh-project-memory/icon.svg +12 -0
- package/plugins/dsh-project-memory/index.js +1240 -0
- package/plugins/dsh-project-memory/locale/en.json +6 -0
- package/plugins/dsh-project-memory/locale/ru.json +6 -0
- package/plugins/dsh-project-memory/locale/zh.json +6 -0
- package/plugins/dsh-project-memory/package.json +26 -0
- package/plugins/dsh-session-move/README.md +67 -0
- package/plugins/dsh-session-move/client.js +719 -0
- package/plugins/dsh-session-move/cordis.patch.yml +5 -0
- package/plugins/dsh-session-move/icon.svg +14 -0
- package/plugins/dsh-session-move/index.js +497 -0
- package/plugins/dsh-session-move/locale/en.json +6 -0
- package/plugins/dsh-session-move/locale/ru.json +6 -0
- package/plugins/dsh-session-move/locale/zh.json +6 -0
- package/plugins/dsh-session-move/package.json +31 -0
- package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
- package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
- package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
- package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
- package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
- package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
- package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
- package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
- package/plugins/dsh-skill-autopilot/index.js +444 -0
- package/plugins/dsh-skill-autopilot/package.json +19 -0
- package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
- package/plugins/dsh-token-lab/lib/client.js +504 -0
- package/plugins/dsh-token-lab/lib/index.js +275 -0
- package/plugins/dsh-token-lab/package.json +26 -0
- package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
- package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
- package/skills/code-review-checklist/SKILL.md +55 -0
- package/skills/content-writing/README.md +87 -0
- package/skills/content-writing/SKILL.md +85 -0
- package/skills/content-writing/assets/checklists/fact-check.md +53 -0
- package/skills/content-writing/assets/checklists/publish.md +65 -0
- package/skills/content-writing/assets/checklists/social.md +40 -0
- package/skills/content-writing/assets/templates/article.md +47 -0
- package/skills/content-writing/assets/templates/brief.md +44 -0
- package/skills/content-writing/assets/templates/doctor-card.md +41 -0
- package/skills/content-writing/assets/templates/image-prompts.md +128 -0
- package/skills/content-writing/assets/templates/post-telegram.md +36 -0
- package/skills/content-writing/assets/templates/post-vk.md +42 -0
- package/skills/content-writing/assets/templates/press-release.md +56 -0
- package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
- package/skills/content-writing/assets/templates/service-card.md +46 -0
- package/skills/content-writing/references/anti-ai-style.md +248 -0
- package/skills/content-writing/references/editing.md +185 -0
- package/skills/content-writing/references/fact-checking.md +205 -0
- package/skills/content-writing/references/government-medical.md +246 -0
- package/skills/content-writing/references/images.md +212 -0
- package/skills/content-writing/references/journalism.md +252 -0
- package/skills/content-writing/references/process.md +123 -0
- package/skills/content-writing/references/quick-recipes.md +99 -0
- package/skills/content-writing/references/rules.md +85 -0
- package/skills/content-writing/references/science-popular.md +218 -0
- package/skills/content-writing/references/seo-content.md +172 -0
- package/skills/content-writing/references/social-media.md +253 -0
- package/skills/content-writing/references/sources.md +49 -0
- package/skills/content-writing/references/templates.md +291 -0
- package/skills/context-economy/SKILL.md +60 -0
- package/skills/context-economy/references/long-session.md +35 -0
- package/skills/context-economy/references/token-math.md +42 -0
- package/skills/context-economy/references/tool-recipes.md +34 -0
- package/skills/database-safety/SKILL.md +47 -0
- package/skills/debugging-playbook/SKILL.md +42 -0
- package/skills/design-critique/SKILL.md +65 -0
- package/skills/design-critique/references/a11y.md +25 -0
- package/skills/design-critique/references/diagnoses.md +33 -0
- package/skills/design-critique/references/quick-tests.md +30 -0
- package/skills/design-critique/references/report-template.md +63 -0
- package/skills/design-critique/references/rubric-fast.md +73 -0
- package/skills/dev-workflow/SKILL.md +48 -0
- package/skills/dev-workflow/references/definition-of-done.md +44 -0
- package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
- package/skills/dev-workflow/references/stages.md +67 -0
- package/skills/git-and-ci/SKILL.md +60 -0
- package/skills/pg-upgrade-verify/SKILL.md +67 -0
- package/skills/project-deep-dive/SKILL.md +45 -0
- package/skills/project-deep-dive/references/recon-commands.md +90 -0
- package/skills/project-deep-dive/references/section-templates.md +104 -0
- package/skills/project-deep-dive/references/tool-and-process.md +86 -0
- package/skills/project-memory-maintenance/SKILL.md +55 -0
- package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
- package/skills/project-memory-maintenance/references/journal-template.md +50 -0
- package/skills/project-memory-maintenance/references/section-rules.md +59 -0
- package/skills/release-and-deploy/SKILL.md +53 -0
- package/skills/requirements-and-planning/SKILL.md +49 -0
- package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
- package/skills/requirements-and-planning/references/process-steps.md +52 -0
- package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
- package/skills/security-review/SKILL.md +40 -0
- package/skills/seo/README.md +60 -0
- package/skills/seo/SKILL.md +77 -0
- package/skills/seo/assets/README.md +66 -0
- package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/seo/assets/scripts/README.md +179 -0
- package/skills/seo/assets/scripts/lib/args.mjs +105 -0
- package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
- package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
- package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
- package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
- package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
- package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
- package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
- package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
- package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
- package/skills/seo/assets/snippets/meta.blade.php +61 -0
- package/skills/seo/assets/snippets/robots.txt +61 -0
- package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
- package/skills/seo/assets/snippets/sitemap.xml +36 -0
- package/skills/seo/references/antipatterns.md +66 -0
- package/skills/seo/references/audit-checklist.md +319 -0
- package/skills/seo/references/content-seo.md +204 -0
- package/skills/seo/references/eeat-medical.md +219 -0
- package/skills/seo/references/measurement.md +250 -0
- package/skills/seo/references/on-page.md +214 -0
- package/skills/seo/references/page-templates.md +56 -0
- package/skills/seo/references/stack-recipes.md +695 -0
- package/skills/seo/references/structured-data.md +487 -0
- package/skills/seo/references/technical-seo.md +296 -0
- package/skills/service-desk-conventions/SKILL.md +52 -0
- package/skills/service-desk-db-safety/SKILL.md +63 -0
- package/skills/service-desk-release/SKILL.md +61 -0
- package/skills/service-desk-testing/SKILL.md +61 -0
- package/skills/service-desk-ui/SKILL.md +52 -0
- package/skills/skills-router/SKILL.md +53 -0
- package/skills/skills-router/references/applying-skills.md +25 -0
- package/skills/skills-router/references/routing-matrix.md +52 -0
- package/skills/testing-strategy/SKILL.md +52 -0
- package/skills/ui-design-system/SKILL.md +50 -0
- package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
- package/skills/ui-design-system/references/tokens-and-components.md +67 -0
- package/skills/ui-design-system/references/visual-qa.md +44 -0
- package/skills/web-research/SKILL.md +47 -0
- package/skills/web-research/references/answer-and-citations.md +60 -0
- package/skills/web-research/references/query-recipes.md +44 -0
- package/skills/wow-design/SKILL.md +89 -0
- package/skills/wow-design/assets/kit/README.md +195 -0
- package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
- package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
- package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
- package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
- package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
- package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
- package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
- package/skills/wow-design/assets/kit/showcase.html +1164 -0
- package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
- package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/wow-design/assets/scripts/README.md +312 -0
- package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
- package/skills/wow-design/assets/scripts/capture.mjs +337 -0
- package/skills/wow-design/assets/scripts/package.json +30 -0
- package/skills/wow-design/references/anti-patterns.md +252 -0
- package/skills/wow-design/references/critique-rubric.md +262 -0
- package/skills/wow-design/references/design-brief-template.md +48 -0
- package/skills/wow-design/references/inspiration.md +203 -0
- package/skills/wow-design/references/presets.md +99 -0
- package/skills/wow-design/references/principles.md +371 -0
- package/skills/wow-design/references/quick-presets.md +50 -0
- package/skills/wow-design/references/report-template.md +21 -0
- package/skills/wow-design/references/stack-recipes.md +96 -0
- package/skills/wow-design/references/visual-qa.md +82 -0
- package/skills/wow-design/references/wow-levers.md +43 -0
- package/tools/skill-cost.mjs +76 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-gkb-ru-ui — skills inventory and usage scanner (Host side).
|
|
3
|
+
*
|
|
4
|
+
* Pure Node module: reads `~/.dsh/skills/*\/SKILL.md`, computes per-skill file
|
|
5
|
+
* stats, and aggregates usage from session logs (zstd frames, JSONL events).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import zlib from "node:zlib";
|
|
12
|
+
|
|
13
|
+
export const CATEGORY_COLORS = {
|
|
14
|
+
design: "#0E6E5E",
|
|
15
|
+
seo: "#2C5F8A",
|
|
16
|
+
content: "#B36A43",
|
|
17
|
+
process: "#6B7275",
|
|
18
|
+
quality: "#1F7A57",
|
|
19
|
+
safety: "#94661C",
|
|
20
|
+
release: "#2C5F8A",
|
|
21
|
+
project: "#8E2B18",
|
|
22
|
+
memory: "#5B4B8A",
|
|
23
|
+
other: "#7A7F87",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SKILL_CATEGORIES = {
|
|
27
|
+
"wow-design": ["design", "Дизайн"],
|
|
28
|
+
"design-critique": ["design", "Дизайн"],
|
|
29
|
+
"ui-design-system": ["design", "Дизайн"],
|
|
30
|
+
seo: ["seo", "SEO"],
|
|
31
|
+
"content-writing": ["content", "Контент"],
|
|
32
|
+
"dev-workflow": ["process", "Процесс"],
|
|
33
|
+
"requirements-and-planning": ["process", "Процесс"],
|
|
34
|
+
"skills-router": ["process", "Процесс"],
|
|
35
|
+
"context-economy": ["process", "Процесс"],
|
|
36
|
+
"requirements-and-planning": ["process", "Процесс"],
|
|
37
|
+
"testing-strategy": ["quality", "Качество"],
|
|
38
|
+
"debugging-playbook": ["quality", "Качество"],
|
|
39
|
+
"code-review-checklist": ["quality", "Качество"],
|
|
40
|
+
"database-safety": ["safety", "Данные"],
|
|
41
|
+
"security-review": ["safety", "Данные"],
|
|
42
|
+
"pg-upgrade-verify": ["safety", "Данные"],
|
|
43
|
+
"release-and-deploy": ["release", "Релиз"],
|
|
44
|
+
"git-and-ci": ["release", "Релиз"],
|
|
45
|
+
"service-desk-conventions": ["project", "Проект"],
|
|
46
|
+
"service-desk-db-safety": ["project", "Проект"],
|
|
47
|
+
"service-desk-testing": ["project", "Проект"],
|
|
48
|
+
"service-desk-release": ["project", "Проект"],
|
|
49
|
+
"service-desk-ui": ["project", "Проект"],
|
|
50
|
+
"project-deep-dive": ["memory", "Память"],
|
|
51
|
+
"project-memory-maintenance": ["memory", "Память"],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
55
|
+
const CACHE_TTL_MS = 30 * 1000;
|
|
56
|
+
const CORE_TOKEN_BUDGET = 6000;
|
|
57
|
+
const MAX_SKILL_BYTES = 200 * 1024;
|
|
58
|
+
|
|
59
|
+
/** Rough token count for mixed Russian/English markdown. */
|
|
60
|
+
export function approxTokens(bytes) {
|
|
61
|
+
return Math.max(40, Math.round(Number(bytes) / 3.2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function skillNameIsValid(name) {
|
|
65
|
+
return typeof name === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) && name.length <= 64;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cache = { key: "", at: 0, data: null };
|
|
69
|
+
|
|
70
|
+
export function invalidateSkillsCache() {
|
|
71
|
+
cache = { key: "", at: 0, data: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function skillsHome() {
|
|
75
|
+
return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function humanSize(bytes) {
|
|
79
|
+
if (bytes < 1024) return `${bytes} Б`;
|
|
80
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
|
|
81
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`;
|
|
82
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} ГБ`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function unquote(value) {
|
|
86
|
+
const text = String(value).trim();
|
|
87
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(text.startsWith("'") ? `"${text.slice(1, -1).replaceAll('"', '\\"')}"` : text);
|
|
90
|
+
} catch {
|
|
91
|
+
return text.slice(1, -1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function parseFrontmatter(text) {
|
|
98
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/u);
|
|
99
|
+
const result = { name: "", description: "", whenToUse: "", userInvocable: false };
|
|
100
|
+
if (match === null) return result;
|
|
101
|
+
for (const line of match[1].split(/\r?\n/u)) {
|
|
102
|
+
const pair = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/u);
|
|
103
|
+
if (pair === null) continue;
|
|
104
|
+
const key = pair[1];
|
|
105
|
+
const value = unquote(pair[2]);
|
|
106
|
+
if (key === "name") result.name = value;
|
|
107
|
+
else if (key === "description") result.description = value;
|
|
108
|
+
else if (key === "whenToUse") result.whenToUse = value;
|
|
109
|
+
else if (key === "user-invocable") result.userInvocable = value === "true";
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function walkStats(directory) {
|
|
115
|
+
let files = 0;
|
|
116
|
+
let bytes = 0;
|
|
117
|
+
let updatedAt = 0;
|
|
118
|
+
let references = 0;
|
|
119
|
+
let assets = 0;
|
|
120
|
+
const walk = (current, depth) => {
|
|
121
|
+
if (depth > 6) return;
|
|
122
|
+
let entries = [];
|
|
123
|
+
try {
|
|
124
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
130
|
+
const full = path.join(current, entry.name);
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
if (depth === 0 && entry.name === "references") references = 1;
|
|
133
|
+
if (depth === 0 && entry.name === "assets") assets = 1;
|
|
134
|
+
walk(full, depth + 1);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const stat = fs.statSync(full);
|
|
139
|
+
files += 1;
|
|
140
|
+
bytes += stat.size;
|
|
141
|
+
if (stat.mtimeMs > updatedAt) updatedAt = stat.mtimeMs;
|
|
142
|
+
} catch {
|
|
143
|
+
/* skip */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
walk(directory, 0);
|
|
148
|
+
return { files, bytes, updatedAt, references, assets };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function firstParagraph(text) {
|
|
152
|
+
const body = text.replace(/^---[\s\S]*?---\r?\n/u, "");
|
|
153
|
+
const paragraphs = body.split(/\r?\n\s*\r?\n/u).map((item) => item.trim()).filter((item) => item.length > 0 && !item.startsWith("#") && !item.startsWith(">"));
|
|
154
|
+
const textOut = paragraphs[0] ?? "";
|
|
155
|
+
return textOut.replace(/[*_`]/gu, "").slice(0, 400);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function listSkills(home = skillsHome()) {
|
|
159
|
+
const directory = path.join(home, "skills");
|
|
160
|
+
let entries = [];
|
|
161
|
+
try {
|
|
162
|
+
entries = fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const skills = [];
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const skillDir = path.join(directory, entry.name);
|
|
169
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
170
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
171
|
+
let text = "";
|
|
172
|
+
try {
|
|
173
|
+
text = fs.readFileSync(skillFile, "utf8");
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const front = parseFrontmatter(text);
|
|
178
|
+
const stats = walkStats(skillDir);
|
|
179
|
+
const category = SKILL_CATEGORIES[entry.name] ?? ["other", "Другое"];
|
|
180
|
+
const skillBytes = Buffer.byteLength(text, "utf8");
|
|
181
|
+
const tokens = approxTokens(skillBytes);
|
|
182
|
+
skills.push({
|
|
183
|
+
name: front.name.length > 0 ? front.name : entry.name,
|
|
184
|
+
directory: entry.name,
|
|
185
|
+
title: entry.name,
|
|
186
|
+
description: front.description.length > 0 ? front.description : firstParagraph(text),
|
|
187
|
+
whenToUse: front.whenToUse,
|
|
188
|
+
userInvocable: front.userInvocable,
|
|
189
|
+
category: category[0],
|
|
190
|
+
categoryLabel: category[1],
|
|
191
|
+
color: CATEGORY_COLORS[category[0]] ?? CATEGORY_COLORS.other,
|
|
192
|
+
files: stats.files,
|
|
193
|
+
bytes: stats.bytes,
|
|
194
|
+
skillBytes,
|
|
195
|
+
tokens,
|
|
196
|
+
heavy: tokens > CORE_TOKEN_BUDGET,
|
|
197
|
+
human: humanSize(stats.bytes),
|
|
198
|
+
updatedAt: stats.updatedAt > 0 ? new Date(stats.updatedAt).toISOString() : null,
|
|
199
|
+
references: stats.references === 1,
|
|
200
|
+
assets: stats.assets === 1,
|
|
201
|
+
path: skillDir,
|
|
202
|
+
skillFile,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return skills;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ------------------------------------------------------------------ */
|
|
209
|
+
/* session log scanning (concatenated zstd frames with JSONL events) */
|
|
210
|
+
/* ------------------------------------------------------------------ */
|
|
211
|
+
|
|
212
|
+
function decodeSessionLog(buffer) {
|
|
213
|
+
const positions = [];
|
|
214
|
+
let from = 0;
|
|
215
|
+
while (true) {
|
|
216
|
+
const index = buffer.indexOf(ZSTD_MAGIC, from);
|
|
217
|
+
if (index < 0) break;
|
|
218
|
+
positions.push(index);
|
|
219
|
+
from = index + 1;
|
|
220
|
+
}
|
|
221
|
+
if (positions.length === 0) return "";
|
|
222
|
+
const chunks = [];
|
|
223
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
224
|
+
const start = positions[index];
|
|
225
|
+
const end = positions[index + 1] ?? buffer.length;
|
|
226
|
+
try {
|
|
227
|
+
chunks.push(zlib.zstdDecompressSync(buffer.subarray(start, end)));
|
|
228
|
+
} catch {
|
|
229
|
+
/* a false magic match inside compressed data */
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function walkSessionFiles(root) {
|
|
236
|
+
const files = [];
|
|
237
|
+
const walk = (dir, depth) => {
|
|
238
|
+
if (depth > 3) return;
|
|
239
|
+
let entries = [];
|
|
240
|
+
try {
|
|
241
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
242
|
+
} catch {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const full = path.join(dir, entry.name);
|
|
247
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
248
|
+
else if (entry.name.endsWith(".zstd")) files.push(full);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
walk(root, 0);
|
|
252
|
+
return files;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sessionSignature(home) {
|
|
256
|
+
const sessionDir = path.join(home, "sessions");
|
|
257
|
+
const files = walkSessionFiles(sessionDir);
|
|
258
|
+
const parts = [];
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
try {
|
|
261
|
+
const stat = fs.statSync(file);
|
|
262
|
+
parts.push(`${path.basename(path.dirname(file))}:${stat.size}:${Math.floor(stat.mtimeMs)}`);
|
|
263
|
+
} catch {
|
|
264
|
+
/* ignore */
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const skillsDir = path.join(home, "skills");
|
|
268
|
+
let skillsStamp = "";
|
|
269
|
+
try {
|
|
270
|
+
const stats = fs.readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
271
|
+
try {
|
|
272
|
+
return `${entry.name}:${fs.statSync(path.join(skillsDir, entry.name)).mtimeMs}`;
|
|
273
|
+
} catch {
|
|
274
|
+
return entry.name;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
skillsStamp = stats.sort().join("|");
|
|
278
|
+
} catch {
|
|
279
|
+
skillsStamp = "";
|
|
280
|
+
}
|
|
281
|
+
return { files, key: `${parts.sort().join(";")}::${skillsStamp}` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function collectUsage(files) {
|
|
285
|
+
const usage = new Map();
|
|
286
|
+
const sessionsById = new Map();
|
|
287
|
+
const dayKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
let text = "";
|
|
290
|
+
try {
|
|
291
|
+
text = decodeSessionLog(fs.readFileSync(file));
|
|
292
|
+
} catch {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
let sessionId = path.basename(path.dirname(file));
|
|
296
|
+
let origin = "user";
|
|
297
|
+
let cwd = "";
|
|
298
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
299
|
+
if (line.length === 0) continue;
|
|
300
|
+
let event;
|
|
301
|
+
try {
|
|
302
|
+
event = JSON.parse(line);
|
|
303
|
+
} catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (event.type === "session") {
|
|
307
|
+
sessionId = event.id ?? sessionId;
|
|
308
|
+
origin = event.origin ?? "user";
|
|
309
|
+
cwd = event.cwd ?? "";
|
|
310
|
+
sessionsById.set(sessionId, { sessionId, origin, cwd, createdAt: event.createdAt ?? null });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (event.type !== "tool/call" || event.data?.name !== "skill") continue;
|
|
314
|
+
let args;
|
|
315
|
+
try {
|
|
316
|
+
args = typeof event.data.arguments === "string" ? JSON.parse(event.data.arguments) : event.data.arguments;
|
|
317
|
+
} catch {
|
|
318
|
+
args = undefined;
|
|
319
|
+
}
|
|
320
|
+
const name = typeof args?.name === "string" ? args.name : undefined;
|
|
321
|
+
if (name === undefined || name.length === 0) continue;
|
|
322
|
+
const at = Number(event.time) || null;
|
|
323
|
+
const record = usage.get(name) ?? { name, count: 0, lastAt: 0, days: new Map(), sessions: new Set() };
|
|
324
|
+
record.count += 1;
|
|
325
|
+
if (at !== null && at > record.lastAt) record.lastAt = at;
|
|
326
|
+
if (at !== null) {
|
|
327
|
+
const key = dayKey(at);
|
|
328
|
+
record.days.set(key, (record.days.get(key) ?? 0) + 1);
|
|
329
|
+
}
|
|
330
|
+
record.sessions.add(sessionId);
|
|
331
|
+
usage.set(name, record);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { usage, sessionsById };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function collectSkillsData(home = skillsHome(), sessionId = null) {
|
|
338
|
+
const { files, key } = sessionSignature(home);
|
|
339
|
+
const cacheKey = `${key}::${sessionId ?? ""}`;
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
if (cache.data !== null && cache.key === cacheKey && now - cache.at < CACHE_TTL_MS) return cache.data;
|
|
342
|
+
const skills = listSkills(home);
|
|
343
|
+
const { usage } = collectUsage(files);
|
|
344
|
+
const today = new Date();
|
|
345
|
+
const days = [];
|
|
346
|
+
for (let index = 13; index >= 0; index -= 1) {
|
|
347
|
+
const date = new Date(today);
|
|
348
|
+
date.setDate(date.getDate() - index);
|
|
349
|
+
days.push(date.toISOString().slice(0, 10));
|
|
350
|
+
}
|
|
351
|
+
let totalRuns = 0;
|
|
352
|
+
const active = [];
|
|
353
|
+
for (const skill of skills) {
|
|
354
|
+
const record = usage.get(skill.name);
|
|
355
|
+
const series = days.map((day) => record?.days.get(day) ?? 0);
|
|
356
|
+
skill.usage = {
|
|
357
|
+
count: record?.count ?? 0,
|
|
358
|
+
lastAt: record?.lastAt ? new Date(record.lastAt).toISOString() : null,
|
|
359
|
+
sessions: record?.sessions.size ?? 0,
|
|
360
|
+
series,
|
|
361
|
+
week: series.slice(-7).reduce((sum, value) => sum + value, 0),
|
|
362
|
+
inCurrentSession: sessionId !== null && (record?.sessions.has(sessionId) ?? false),
|
|
363
|
+
};
|
|
364
|
+
totalRuns += skill.usage.count;
|
|
365
|
+
if (sessionId !== null && (record?.sessions.has(sessionId) ?? false)) {
|
|
366
|
+
active.push({ name: skill.name, lastAt: skill.usage.lastAt, count: record.count });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
active.sort((left, right) => (right.lastAt ?? "").localeCompare(left.lastAt ?? ""));
|
|
370
|
+
const data = {
|
|
371
|
+
skills,
|
|
372
|
+
totals: {
|
|
373
|
+
skills: skills.length,
|
|
374
|
+
used: skills.filter((skill) => skill.usage.count > 0).length,
|
|
375
|
+
runs: totalRuns,
|
|
376
|
+
active: active.length,
|
|
377
|
+
tokens: skills.reduce((sum, skill) => sum + (skill.tokens || 0), 0),
|
|
378
|
+
heavy: skills.filter((skill) => skill.heavy).length,
|
|
379
|
+
references: skills.filter((skill) => skill.references).length,
|
|
380
|
+
budget: CORE_TOKEN_BUDGET,
|
|
381
|
+
},
|
|
382
|
+
active,
|
|
383
|
+
categories: Object.entries(CATEGORY_COLORS).map(([id, color]) => ({ id, label: (skills.find((skill) => skill.category === id)?.categoryLabel) ?? id, color })),
|
|
384
|
+
generatedAt: new Date().toISOString(),
|
|
385
|
+
days,
|
|
386
|
+
};
|
|
387
|
+
cache = { key: cacheKey, at: now, data };
|
|
388
|
+
return data;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function readSkillContent(home, name) {
|
|
392
|
+
const skills = listSkills(home);
|
|
393
|
+
const skill = skills.find((item) => item.name === name || item.directory === name);
|
|
394
|
+
if (skill === undefined) return null;
|
|
395
|
+
let content = "";
|
|
396
|
+
try {
|
|
397
|
+
content = fs.readFileSync(skill.skillFile, "utf8");
|
|
398
|
+
} catch {
|
|
399
|
+
content = "";
|
|
400
|
+
}
|
|
401
|
+
return { ...skill, content, resources: listSkillResources(skill.path) };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* ------------------------------------------------------------------ */
|
|
405
|
+
/* skill studio: read resources, write, create, duplicate, archive */
|
|
406
|
+
/* ------------------------------------------------------------------ */
|
|
407
|
+
|
|
408
|
+
function listSkillResources(skillDir) {
|
|
409
|
+
const resources = [];
|
|
410
|
+
const walk = (current, depth) => {
|
|
411
|
+
if (depth > 3) return;
|
|
412
|
+
let entries = [];
|
|
413
|
+
try {
|
|
414
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
415
|
+
} catch {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
for (const entry of entries) {
|
|
419
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
420
|
+
const full = path.join(current, entry.name);
|
|
421
|
+
if (entry.isDirectory()) {
|
|
422
|
+
walk(full, depth + 1);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (entry.name === "SKILL.md") continue;
|
|
426
|
+
try {
|
|
427
|
+
const stat = fs.statSync(full);
|
|
428
|
+
if (!/\.(md|txt|json|ya?ml)$/u.test(entry.name)) continue;
|
|
429
|
+
resources.push({
|
|
430
|
+
path: path.relative(skillDir, full).replaceAll("\\", "/"),
|
|
431
|
+
bytes: stat.size,
|
|
432
|
+
tokens: approxTokens(stat.size),
|
|
433
|
+
});
|
|
434
|
+
} catch {
|
|
435
|
+
/* skip */
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
walk(skillDir, 0);
|
|
440
|
+
resources.sort((left, right) => right.tokens - left.tokens);
|
|
441
|
+
return resources;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveSkill(home, name) {
|
|
445
|
+
const skills = listSkills(home);
|
|
446
|
+
return skills.find((item) => item.name === name || item.directory === name) ?? null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function backupFile(home, skillFile, name) {
|
|
450
|
+
try {
|
|
451
|
+
if (!fs.existsSync(skillFile)) return null;
|
|
452
|
+
const directory = path.join(home, "skills", ".dsh-kit-backups");
|
|
453
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
454
|
+
const stamp = new Date().toISOString().replace(/[:.]/gu, "-");
|
|
455
|
+
const target = path.join(directory, `${name}-${stamp}.md`);
|
|
456
|
+
fs.copyFileSync(skillFile, target);
|
|
457
|
+
return target;
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function assertContent(content) {
|
|
464
|
+
if (typeof content !== "string") throw Object.assign(new Error("content должен быть строкой"), { code: "skills/bad-request" });
|
|
465
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
466
|
+
if (bytes > MAX_SKILL_BYTES) {
|
|
467
|
+
throw Object.assign(new Error(`слишком большой SKILL.md: ${bytes} байт > ${MAX_SKILL_BYTES}`), { code: "skills/too-large" });
|
|
468
|
+
}
|
|
469
|
+
const front = content.match(/^---\r?\n([\s\S]*?)\r?\n---/u);
|
|
470
|
+
if (front === null) {
|
|
471
|
+
throw Object.assign(new Error("нет frontmatter: файл должен начинаться с --- и полей name/description"), { code: "skills/bad-request" });
|
|
472
|
+
}
|
|
473
|
+
const nameLine = front[1].match(/^name:\s*(.+)$/mu);
|
|
474
|
+
if (nameLine === null) {
|
|
475
|
+
throw Object.assign(new Error("во frontmatter нет поля name"), { code: "skills/bad-request" });
|
|
476
|
+
}
|
|
477
|
+
return { bytes, frontName: unquote(nameLine[1]) };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function saveSkillContent(home, name, content) {
|
|
481
|
+
const skill = resolveSkill(home, name);
|
|
482
|
+
if (skill === null) throw Object.assign(new Error(`навык «${name}» не найден`), { code: "skills/not-found" });
|
|
483
|
+
const { bytes, frontName } = assertContent(content);
|
|
484
|
+
if (frontName !== skill.directory) {
|
|
485
|
+
throw Object.assign(new Error(`поле name (${frontName}) не совпадает с папкой (${skill.directory}); переименование — через дубликат`), { code: "skills/bad-request" });
|
|
486
|
+
}
|
|
487
|
+
const backup = backupFile(home, skill.skillFile, skill.directory);
|
|
488
|
+
const temp = `${skill.skillFile}.tmp-${process.pid}-${Date.now()}`;
|
|
489
|
+
fs.writeFileSync(temp, content, "utf8");
|
|
490
|
+
fs.renameSync(temp, skill.skillFile);
|
|
491
|
+
invalidateSkillsCache();
|
|
492
|
+
return { ...readSkillContent(home, skill.name), bytes, backup, savedAt: new Date().toISOString() };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function skillTemplate(name, description) {
|
|
496
|
+
return `---
|
|
497
|
+
name: ${name}
|
|
498
|
+
description: "${String(description ?? "").replaceAll('"', "'").slice(0, 200)}"
|
|
499
|
+
whenToUse: "Когда задача совпадает с триггерами ниже"
|
|
500
|
+
user-invocable: true
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
# ${name} — ядро
|
|
504
|
+
|
|
505
|
+
> ≈400 токенов. Это ядро; детали — в references и только когда реально нужны.
|
|
506
|
+
|
|
507
|
+
## 0. Когда применять
|
|
508
|
+
- ...
|
|
509
|
+
|
|
510
|
+
## 1. Главное
|
|
511
|
+
1. ...
|
|
512
|
+
|
|
513
|
+
## 2. Куда грузить дальше
|
|
514
|
+
| Нужно | Файл | ≈токенов |
|
|
515
|
+
|---|---|---|
|
|
516
|
+
| ... | references/....md | ≈... |
|
|
517
|
+
|
|
518
|
+
## 3. Гейт перед сдачей
|
|
519
|
+
- [ ] ...
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function createSkill(home, payload) {
|
|
524
|
+
const name = String(payload?.name ?? "").trim();
|
|
525
|
+
if (!skillNameIsValid(name)) {
|
|
526
|
+
throw Object.assign(new Error("имя навыка: только kebab-case [a-z0-9-], до 64 символов"), { code: "skills/bad-request" });
|
|
527
|
+
}
|
|
528
|
+
const directory = path.join(home, "skills", name);
|
|
529
|
+
if (fs.existsSync(directory)) {
|
|
530
|
+
throw Object.assign(new Error(`навык «${name}» уже существует`), { code: "skills/exists" });
|
|
531
|
+
}
|
|
532
|
+
const content = typeof payload?.content === "string" && payload.content.trim().length > 0
|
|
533
|
+
? payload.content
|
|
534
|
+
: skillTemplate(name, payload?.description);
|
|
535
|
+
assertContent(content);
|
|
536
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
537
|
+
fs.writeFileSync(path.join(directory, "SKILL.md"), content, "utf8");
|
|
538
|
+
invalidateSkillsCache();
|
|
539
|
+
return readSkillContent(home, name);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function duplicateSkill(home, source, nextName) {
|
|
543
|
+
const skill = resolveSkill(home, source);
|
|
544
|
+
if (skill === null) throw Object.assign(new Error(`навык «${source}» не найден`), { code: "skills/not-found" });
|
|
545
|
+
const name = String(nextName ?? "").trim();
|
|
546
|
+
if (!skillNameIsValid(name)) {
|
|
547
|
+
throw Object.assign(new Error("новое имя: только kebab-case [a-z0-9-]"), { code: "skills/bad-request" });
|
|
548
|
+
}
|
|
549
|
+
const target = path.join(home, "skills", name);
|
|
550
|
+
if (fs.existsSync(target)) throw Object.assign(new Error(`навык «${name}» уже существует`), { code: "skills/exists" });
|
|
551
|
+
fs.cpSync(skill.path, target, { recursive: true });
|
|
552
|
+
const targetFile = path.join(target, "SKILL.md");
|
|
553
|
+
const text = fs.readFileSync(targetFile, "utf8");
|
|
554
|
+
const rewritten = text.replace(/^(---\r?\n[\s\S]*?)^name:\s*.+$/mu, `$1name: ${name}`);
|
|
555
|
+
fs.writeFileSync(targetFile, rewritten, "utf8");
|
|
556
|
+
invalidateSkillsCache();
|
|
557
|
+
return readSkillContent(home, name);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function deleteSkill(home, name) {
|
|
561
|
+
const skill = resolveSkill(home, name);
|
|
562
|
+
if (skill === null) throw Object.assign(new Error(`навык «${name}» не найден`), { code: "skills/not-found" });
|
|
563
|
+
const trash = path.join(home, "skills", ".dsh-kit-trash");
|
|
564
|
+
fs.mkdirSync(trash, { recursive: true });
|
|
565
|
+
const stamp = new Date().toISOString().replace(/[:.]/gu, "-");
|
|
566
|
+
const target = path.join(trash, `${stamp}-${skill.directory}`);
|
|
567
|
+
fs.renameSync(skill.path, target);
|
|
568
|
+
invalidateSkillsCache();
|
|
569
|
+
return { deleted: skill.directory, trash: target };
|
|
570
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-gkb-ru-ui",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Russian language pack for the DSH web GUI + compact AURORA sidebar tiles (skills/tokens/balance/spend) + Skills Studio: catalog, token cost, inline SKILL.md editor, create/duplicate/archive.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"default": "./lib/client.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"dsh": {
|
|
17
|
+
"manifestVersion": 1,
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
},
|
|
21
|
+
"client": {
|
|
22
|
+
"platform": "web",
|
|
23
|
+
"inject": [
|
|
24
|
+
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# dsh-peak-guard — живое уведомление о пиковом тарифе DeepSeek
|
|
2
|
+
|
|
3
|
+
Клиентский плагин для DeepSeek Harness: пока действует пиковый тариф (токены ×2),
|
|
4
|
+
в чате над композером показывается красная карточка, а в заголовке сессии —
|
|
5
|
+
компактный бейдж «Пик ×2 · осталось N мин».
|
|
6
|
+
|
|
7
|
+
## Что показывает уведомление
|
|
8
|
+
|
|
9
|
+
### За 5 минут до пика — жёлтое предупреждение
|
|
10
|
+
|
|
11
|
+
Когда до начала пикового окна остаётся **5 минут** (настраивается константой
|
|
12
|
+
`PEAK_LEAD_MS`), появляется янтарная карточка «Скоро пик ×2» и бейдж
|
|
13
|
+
«Пик через N мин»: причина, время старта, живой отсчёт до начала, совет
|
|
14
|
+
«завершите текущий шаг до 09:00 — или вернитесь после 13:00».
|
|
15
|
+
Кнопка «Скрыть до начала пика» убирает именно предупреждение; в момент старта
|
|
16
|
+
пика карточка сменится на красную (если не была скрыта уже на пик).
|
|
17
|
+
|
|
18
|
+
**Плагин ничего не останавливает и не закрывает:** он только информирует.
|
|
19
|
+
Сессии, история и файлы сохраняются, чат можно продолжить в любой момент.
|
|
20
|
+
|
|
21
|
+
- причину: в пиковые часы цена **всех** типов токенов для `deepseek-flash` и
|
|
22
|
+
`deepseek-v4-pro` увеличивается вдвое;
|
|
23
|
+
- текущее окно пика в **местном времени** и по UTC, прогресс окна;
|
|
24
|
+
- **живой обратный отсчёт** до конца пика (обновляется каждую секунду);
|
|
25
|
+
- рекомендацию: «Вернитесь после 13:00 (через 26 мин) — цена упадёт вдвое»;
|
|
26
|
+
- расписание пика по будням и напоминание, что выходные — без пика;
|
|
27
|
+
- кнопку «Скрыть на этот пик» (до конца текущего окна; в следующее окно карточка
|
|
28
|
+
появится снова) и «Расписание» для подробностей.
|
|
29
|
+
|
|
30
|
+
## Расписание
|
|
31
|
+
|
|
32
|
+
По референсу владельца (UTC, по будням):
|
|
33
|
+
|
|
34
|
+
| Сессия | UTC | Москва (MSK) | Пекин (CST) |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| Утренняя | 01:00–04:00 | 04:00–07:00 | 09:00–12:00 |
|
|
37
|
+
| Дневная | 06:00–10:00 | 09:00–13:00 | 14:00–18:00 |
|
|
38
|
+
|
|
39
|
+
Всё остальное время — off-peak со скидкой 50%. Будни определяются по UTC; выходные
|
|
40
|
+
пика не имеют. Государственные праздники Китая по расписанию не вычисляются.
|
|
41
|
+
|
|
42
|
+
## Реальное время
|
|
43
|
+
|
|
44
|
+
- тик каждую секунду: обратный отсчёт и прогресс-бар не «застывают»;
|
|
45
|
+
- отдельный таймер на границу окна: карточка появляется/исчезает в момент начала
|
|
46
|
+
или конца пика (а не через полчаса);
|
|
47
|
+
- при возврате фокуса/вкладки время пересчитывается сразу, даже если браузер
|
|
48
|
+
троттлил таймеры в фоне;
|
|
49
|
+
- при открытии чата посреди пика карточка видна сразу.
|
|
50
|
+
|
|
51
|
+
## Хранение
|
|
52
|
+
|
|
53
|
+
`localStorage`:
|
|
54
|
+
|
|
55
|
+
- `dsh-peak-guard:dismissed-window-end` — до какого окна скрыта карточка;
|
|
56
|
+
- `dsh-peak-guard:enabled` — `0` отключает уведомления (служебное; в UI нет).
|
|
57
|
+
|
|
58
|
+
## Отладка/проверка
|
|
59
|
+
|
|
60
|
+
`window.__DSH_PEAK_GUARD__`:
|
|
61
|
+
|
|
62
|
+
- `state()` — текущее вычисленное состояние (пик/нет, окно, остаток);
|
|
63
|
+
- `preview('2026-11-04T12:34:00+03:00')` — принудительно выставить время для
|
|
64
|
+
скриншотов и проверок; `clearPreview()` — вернуть реальное время.
|
|
65
|
+
|
|
66
|
+
## Ограничения
|
|
67
|
+
|
|
68
|
+
- Расписание зашито в плагин; при изменении тарифов правится `WINDOWS_UTC` в
|
|
69
|
+
`client.js` (или добавляется конфиг в будущем).
|
|
70
|
+
- Уведомление внутри DSH; системные push-уведомления не отправляются.
|