dsh-desktop-kit 0.0.0-stage → 1.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.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/client.js +3349 -0
  4. package/cordis.patch.yml +5 -0
  5. package/index.js +80 -0
  6. package/package.json +60 -4
  7. package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
  8. package/plugins/dsh-gkb-ru-ui/lib/client.js +1083 -0
  9. package/plugins/dsh-gkb-ru-ui/lib/index.js +142 -0
  10. package/plugins/dsh-gkb-ru-ui/lib/skills.js +376 -0
  11. package/plugins/dsh-gkb-ru-ui/package.json +28 -0
  12. package/plugins/dsh-peak-guard/README.md +70 -0
  13. package/plugins/dsh-peak-guard/client.js +741 -0
  14. package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
  15. package/plugins/dsh-peak-guard/icon.svg +12 -0
  16. package/plugins/dsh-peak-guard/index.js +2 -0
  17. package/plugins/dsh-peak-guard/locale/en.json +6 -0
  18. package/plugins/dsh-peak-guard/locale/ru.json +6 -0
  19. package/plugins/dsh-peak-guard/locale/zh.json +6 -0
  20. package/plugins/dsh-peak-guard/package.json +33 -0
  21. package/plugins/dsh-profile-transfer/README.md +62 -0
  22. package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
  23. package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
  24. 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
  25. package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
  26. 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
  27. package/plugins/dsh-profile-transfer/client.js +806 -0
  28. package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
  29. package/plugins/dsh-profile-transfer/icon.svg +13 -0
  30. package/plugins/dsh-profile-transfer/index.js +375 -0
  31. package/plugins/dsh-profile-transfer/locale/en.json +6 -0
  32. package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
  33. package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
  34. package/plugins/dsh-profile-transfer/package.json +36 -0
  35. package/plugins/dsh-profile-transfer/transfer.js +1004 -0
  36. package/plugins/dsh-project-memory/README.md +71 -0
  37. package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
  38. package/plugins/dsh-project-memory/icon.svg +12 -0
  39. package/plugins/dsh-project-memory/index.js +1240 -0
  40. package/plugins/dsh-project-memory/locale/en.json +6 -0
  41. package/plugins/dsh-project-memory/locale/ru.json +6 -0
  42. package/plugins/dsh-project-memory/locale/zh.json +6 -0
  43. package/plugins/dsh-project-memory/package.json +26 -0
  44. package/plugins/dsh-session-move/README.md +67 -0
  45. package/plugins/dsh-session-move/client.js +719 -0
  46. package/plugins/dsh-session-move/cordis.patch.yml +5 -0
  47. package/plugins/dsh-session-move/icon.svg +14 -0
  48. package/plugins/dsh-session-move/index.js +497 -0
  49. package/plugins/dsh-session-move/locale/en.json +6 -0
  50. package/plugins/dsh-session-move/locale/ru.json +6 -0
  51. package/plugins/dsh-session-move/locale/zh.json +6 -0
  52. package/plugins/dsh-session-move/package.json +31 -0
  53. package/skills/code-review-checklist/SKILL.md +37 -0
  54. package/skills/content-writing/README.md +87 -0
  55. package/skills/content-writing/SKILL.md +348 -0
  56. package/skills/content-writing/assets/checklists/fact-check.md +53 -0
  57. package/skills/content-writing/assets/checklists/publish.md +65 -0
  58. package/skills/content-writing/assets/checklists/social.md +40 -0
  59. package/skills/content-writing/assets/templates/article.md +47 -0
  60. package/skills/content-writing/assets/templates/brief.md +44 -0
  61. package/skills/content-writing/assets/templates/doctor-card.md +41 -0
  62. package/skills/content-writing/assets/templates/image-prompts.md +128 -0
  63. package/skills/content-writing/assets/templates/post-telegram.md +36 -0
  64. package/skills/content-writing/assets/templates/post-vk.md +42 -0
  65. package/skills/content-writing/assets/templates/press-release.md +56 -0
  66. package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
  67. package/skills/content-writing/assets/templates/service-card.md +46 -0
  68. package/skills/content-writing/references/anti-ai-style.md +248 -0
  69. package/skills/content-writing/references/editing.md +185 -0
  70. package/skills/content-writing/references/fact-checking.md +205 -0
  71. package/skills/content-writing/references/government-medical.md +246 -0
  72. package/skills/content-writing/references/images.md +212 -0
  73. package/skills/content-writing/references/journalism.md +252 -0
  74. package/skills/content-writing/references/science-popular.md +218 -0
  75. package/skills/content-writing/references/seo-content.md +172 -0
  76. package/skills/content-writing/references/social-media.md +253 -0
  77. package/skills/content-writing/references/templates.md +291 -0
  78. package/skills/database-safety/SKILL.md +31 -0
  79. package/skills/debugging-playbook/SKILL.md +39 -0
  80. package/skills/design-critique/SKILL.md +118 -0
  81. package/skills/dev-workflow/SKILL.md +41 -0
  82. package/skills/git-and-ci/SKILL.md +40 -0
  83. package/skills/pg-upgrade-verify/SKILL.md +48 -0
  84. package/skills/project-deep-dive/SKILL.md +170 -0
  85. package/skills/project-deep-dive/references/recon-commands.md +78 -0
  86. package/skills/project-deep-dive/references/section-templates.md +102 -0
  87. package/skills/project-memory-maintenance/SKILL.md +170 -0
  88. package/skills/project-memory-maintenance/references/journal-template.md +48 -0
  89. package/skills/project-memory-maintenance/references/section-rules.md +57 -0
  90. package/skills/release-and-deploy/SKILL.md +39 -0
  91. package/skills/requirements-and-planning/SKILL.md +115 -0
  92. package/skills/security-review/SKILL.md +36 -0
  93. package/skills/seo/README.md +60 -0
  94. package/skills/seo/SKILL.md +329 -0
  95. package/skills/seo/assets/README.md +66 -0
  96. package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
  97. package/skills/seo/assets/scripts/README.md +179 -0
  98. package/skills/seo/assets/scripts/lib/args.mjs +105 -0
  99. package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
  100. package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
  101. package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
  102. package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
  103. package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
  104. package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
  105. package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
  106. package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
  107. package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
  108. package/skills/seo/assets/snippets/meta.blade.php +61 -0
  109. package/skills/seo/assets/snippets/robots.txt +61 -0
  110. package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
  111. package/skills/seo/assets/snippets/sitemap.xml +36 -0
  112. package/skills/seo/references/audit-checklist.md +305 -0
  113. package/skills/seo/references/content-seo.md +198 -0
  114. package/skills/seo/references/eeat-medical.md +217 -0
  115. package/skills/seo/references/measurement.md +223 -0
  116. package/skills/seo/references/on-page.md +209 -0
  117. package/skills/seo/references/stack-recipes.md +693 -0
  118. package/skills/seo/references/structured-data.md +485 -0
  119. package/skills/seo/references/technical-seo.md +294 -0
  120. package/skills/service-desk-conventions/SKILL.md +40 -0
  121. package/skills/service-desk-db-safety/SKILL.md +52 -0
  122. package/skills/service-desk-release/SKILL.md +43 -0
  123. package/skills/service-desk-testing/SKILL.md +47 -0
  124. package/skills/service-desk-ui/SKILL.md +33 -0
  125. package/skills/skills-router/SKILL.md +91 -0
  126. package/skills/testing-strategy/SKILL.md +37 -0
  127. package/skills/ui-design-system/SKILL.md +45 -0
  128. package/skills/web-research/SKILL.md +40 -0
  129. package/skills/wow-design/SKILL.md +168 -0
  130. package/skills/wow-design/assets/kit/README.md +195 -0
  131. package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
  132. package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
  133. package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
  134. package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
  135. package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
  136. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
  137. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
  138. package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
  139. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
  140. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
  141. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
  142. package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
  143. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
  144. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
  145. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
  146. package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
  147. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
  148. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
  149. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
  150. package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
  151. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
  152. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
  153. package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
  154. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
  155. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
  156. package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
  157. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
  158. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
  159. package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
  160. package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
  161. package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
  162. package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
  163. package/skills/wow-design/assets/kit/showcase.html +1164 -0
  164. package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
  165. package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
  166. package/skills/wow-design/assets/scripts/README.md +312 -0
  167. package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
  168. package/skills/wow-design/assets/scripts/capture.mjs +337 -0
  169. package/skills/wow-design/assets/scripts/package.json +30 -0
  170. package/skills/wow-design/references/anti-patterns.md +249 -0
  171. package/skills/wow-design/references/critique-rubric.md +259 -0
  172. package/skills/wow-design/references/design-brief-template.md +46 -0
  173. package/skills/wow-design/references/inspiration.md +200 -0
  174. package/skills/wow-design/references/presets.md +96 -0
  175. package/skills/wow-design/references/principles.md +368 -0
  176. package/skills/wow-design/references/stack-recipes.md +93 -0
  177. package/skills/wow-design/references/visual-qa.md +79 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * dsh-gkb-ru-ui — Host half.
3
+ *
4
+ * Russian language pack marker + Skills catalog RPC:
5
+ * skills — inventory of ~/.dsh/skills with usage statistics from sessions
6
+ * skill — one skill with its SKILL.md content
7
+ * open-skill — reveal a skill folder / SKILL.md in the OS file manager
8
+ */
9
+
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { spawn } from "node:child_process";
14
+ import { collectSkillsData, listSkills, readSkillContent, skillsHome } from "./skills.js";
15
+
16
+ const ROUTE_BASE = "/api/dsh-gkb-ru-ui";
17
+ const RPC_NAMESPACE = "dsh-gkb-ru-ui";
18
+
19
+ export const inject = ["connection"];
20
+
21
+ function okEnvelope(rpcId, value) {
22
+ return Response.json({ type: "server-response", rpcId, result: { ok: true, value } });
23
+ }
24
+
25
+ function errorEnvelope(rpcId, code, message) {
26
+ return Response.json({ type: "server-response", rpcId, result: { ok: false, error: { code, message } } });
27
+ }
28
+
29
+ async function readEnvelope(request) {
30
+ let body;
31
+ try {
32
+ body = await request.json();
33
+ } catch {
34
+ const error = new Error("тело запроса не JSON");
35
+ error.code = "skills/bad-request";
36
+ throw error;
37
+ }
38
+ if (body === null || typeof body !== "object" || body.type !== "client-request" || typeof body.rpcId !== "string" || typeof body.method !== "string") {
39
+ const error = new Error("некорректный конверт Connection");
40
+ error.code = "skills/bad-request";
41
+ throw error;
42
+ }
43
+ return { rpcId: body.rpcId, method: body.method, payload: body.payload ?? {} };
44
+ }
45
+
46
+ function openInExplorer(target, isFile) {
47
+ const platform = process.platform;
48
+ if (platform === "win32") {
49
+ const child = spawn("explorer.exe", [isFile ? `/select,${target}` : target], { detached: true, stdio: "ignore" });
50
+ child.unref?.();
51
+ return true;
52
+ }
53
+ const command = platform === "darwin" ? "open" : "xdg-open";
54
+ const child = spawn(command, [isFile ? path.dirname(target) : target], { detached: true, stdio: "ignore" });
55
+ child.unref?.();
56
+ return true;
57
+ }
58
+
59
+ function registerOperation(ctx, operation, handler) {
60
+ const route = {
61
+ path: `${ROUTE_BASE}/${operation}`,
62
+ methods: ["POST"],
63
+ requestBody: "buffered",
64
+ fetch: async (request) => {
65
+ let rpcId = "invalid-request";
66
+ try {
67
+ const envelope = await readEnvelope(request);
68
+ rpcId = envelope.rpcId;
69
+ if (envelope.method !== `${RPC_NAMESPACE}/${operation}`) {
70
+ return errorEnvelope(rpcId, "skills/bad-request", "метод конверта не совпадает с endpoint");
71
+ }
72
+ const value = await handler(envelope.payload);
73
+ return okEnvelope(rpcId, value);
74
+ } catch (error) {
75
+ const code = typeof error?.code === "string" ? error.code : "skills/internal";
76
+ if (code === "skills/internal") ctx.logger?.warn?.(`[gkb-skills] ${operation} failed: ${String(error)}`);
77
+ return errorEnvelope(rpcId, code, error instanceof Error ? error.message : String(error));
78
+ }
79
+ },
80
+ };
81
+ ctx.effect(() => {
82
+ const dispose = ctx.connection.fetch.register(route);
83
+ return () => {
84
+ try {
85
+ void Promise.resolve(dispose()).catch(() => {});
86
+ } catch {
87
+ /* carrier unloading */
88
+ }
89
+ };
90
+ }, `gkb-ru: ${operation} route`);
91
+ }
92
+
93
+ export function apply(ctx) {
94
+ try {
95
+ fs.writeFileSync(
96
+ path.join(os.tmpdir(), "dsh-gkb-ru-ui-loaded.json"),
97
+ JSON.stringify({ at: new Date().toISOString(), pid: process.pid, version: "1.8.0" }),
98
+ "utf8",
99
+ );
100
+ } catch {
101
+ /* marker is best effort */
102
+ }
103
+
104
+ registerOperation(ctx, "skills", async (payload) => {
105
+ const sessionId = typeof payload?.sessionId === "string" && payload.sessionId.length > 0 ? payload.sessionId : null;
106
+ const home = skillsHome();
107
+ const data = collectSkillsData(home, sessionId);
108
+ return { ...data, home };
109
+ });
110
+
111
+ registerOperation(ctx, "skill", async (payload) => {
112
+ const name = typeof payload?.name === "string" ? payload.name : "";
113
+ if (name.length === 0) {
114
+ const error = new Error("не указано имя навыка");
115
+ error.code = "skills/bad-request";
116
+ throw error;
117
+ }
118
+ const home = skillsHome();
119
+ const skill = readSkillContent(home, name);
120
+ if (skill === null) {
121
+ const error = new Error(`навык «${name}» не найден`);
122
+ error.code = "skills/not-found";
123
+ throw error;
124
+ }
125
+ const data = collectSkillsData(home, typeof payload?.sessionId === "string" ? payload.sessionId : null);
126
+ const usage = data.skills.find((item) => item.name === skill.name)?.usage ?? null;
127
+ return { skill, usage };
128
+ });
129
+
130
+ registerOperation(ctx, "open-skill", async (payload) => {
131
+ const name = typeof payload?.name === "string" ? payload.name : "";
132
+ const home = skillsHome();
133
+ const skill = listSkills(home).find((item) => item.name === name || item.directory === name);
134
+ if (skill === undefined) {
135
+ const error = new Error(`навык «${name}» не найден`);
136
+ error.code = "skills/not-found";
137
+ throw error;
138
+ }
139
+ openInExplorer(skill.skillFile, true);
140
+ return { opened: true, path: skill.path };
141
+ });
142
+ }
@@ -0,0 +1,376 @@
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
+ "testing-strategy": ["quality", "Качество"],
36
+ "debugging-playbook": ["quality", "Качество"],
37
+ "code-review-checklist": ["quality", "Качество"],
38
+ "database-safety": ["safety", "Данные"],
39
+ "security-review": ["safety", "Данные"],
40
+ "pg-upgrade-verify": ["safety", "Данные"],
41
+ "release-and-deploy": ["release", "Релиз"],
42
+ "git-and-ci": ["release", "Релиз"],
43
+ "service-desk-conventions": ["project", "Проект"],
44
+ "service-desk-db-safety": ["project", "Проект"],
45
+ "service-desk-testing": ["project", "Проект"],
46
+ "service-desk-release": ["project", "Проект"],
47
+ "service-desk-ui": ["project", "Проект"],
48
+ "project-deep-dive": ["memory", "Память"],
49
+ "project-memory-maintenance": ["memory", "Память"],
50
+ };
51
+
52
+ const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
53
+ const CACHE_TTL_MS = 30 * 1000;
54
+
55
+ let cache = { key: "", at: 0, data: null };
56
+
57
+ export function skillsHome() {
58
+ return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
59
+ }
60
+
61
+ function humanSize(bytes) {
62
+ if (bytes < 1024) return `${bytes} Б`;
63
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
64
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`;
65
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} ГБ`;
66
+ }
67
+
68
+ function unquote(value) {
69
+ const text = String(value).trim();
70
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
71
+ try {
72
+ return JSON.parse(text.startsWith("'") ? `"${text.slice(1, -1).replaceAll('"', '\\"')}"` : text);
73
+ } catch {
74
+ return text.slice(1, -1);
75
+ }
76
+ }
77
+ return text;
78
+ }
79
+
80
+ export function parseFrontmatter(text) {
81
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/u);
82
+ const result = { name: "", description: "", whenToUse: "", userInvocable: false };
83
+ if (match === null) return result;
84
+ for (const line of match[1].split(/\r?\n/u)) {
85
+ const pair = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/u);
86
+ if (pair === null) continue;
87
+ const key = pair[1];
88
+ const value = unquote(pair[2]);
89
+ if (key === "name") result.name = value;
90
+ else if (key === "description") result.description = value;
91
+ else if (key === "whenToUse") result.whenToUse = value;
92
+ else if (key === "user-invocable") result.userInvocable = value === "true";
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function walkStats(directory) {
98
+ let files = 0;
99
+ let bytes = 0;
100
+ let updatedAt = 0;
101
+ let references = 0;
102
+ let assets = 0;
103
+ const walk = (current, depth) => {
104
+ if (depth > 6) return;
105
+ let entries = [];
106
+ try {
107
+ entries = fs.readdirSync(current, { withFileTypes: true });
108
+ } catch {
109
+ return;
110
+ }
111
+ for (const entry of entries) {
112
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
113
+ const full = path.join(current, entry.name);
114
+ if (entry.isDirectory()) {
115
+ if (depth === 0 && entry.name === "references") references = 1;
116
+ if (depth === 0 && entry.name === "assets") assets = 1;
117
+ walk(full, depth + 1);
118
+ continue;
119
+ }
120
+ try {
121
+ const stat = fs.statSync(full);
122
+ files += 1;
123
+ bytes += stat.size;
124
+ if (stat.mtimeMs > updatedAt) updatedAt = stat.mtimeMs;
125
+ } catch {
126
+ /* skip */
127
+ }
128
+ }
129
+ };
130
+ walk(directory, 0);
131
+ return { files, bytes, updatedAt, references, assets };
132
+ }
133
+
134
+ function firstParagraph(text) {
135
+ const body = text.replace(/^---[\s\S]*?---\r?\n/u, "");
136
+ const paragraphs = body.split(/\r?\n\s*\r?\n/u).map((item) => item.trim()).filter((item) => item.length > 0 && !item.startsWith("#") && !item.startsWith(">"));
137
+ const textOut = paragraphs[0] ?? "";
138
+ return textOut.replace(/[*_`]/gu, "").slice(0, 400);
139
+ }
140
+
141
+ export function listSkills(home = skillsHome()) {
142
+ const directory = path.join(home, "skills");
143
+ let entries = [];
144
+ try {
145
+ entries = fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory());
146
+ } catch {
147
+ return [];
148
+ }
149
+ const skills = [];
150
+ for (const entry of entries) {
151
+ const skillDir = path.join(directory, entry.name);
152
+ const skillFile = path.join(skillDir, "SKILL.md");
153
+ if (!fs.existsSync(skillFile)) continue;
154
+ let text = "";
155
+ try {
156
+ text = fs.readFileSync(skillFile, "utf8");
157
+ } catch {
158
+ continue;
159
+ }
160
+ const front = parseFrontmatter(text);
161
+ const stats = walkStats(skillDir);
162
+ const category = SKILL_CATEGORIES[entry.name] ?? ["other", "Другое"];
163
+ skills.push({
164
+ name: front.name.length > 0 ? front.name : entry.name,
165
+ directory: entry.name,
166
+ title: entry.name,
167
+ description: front.description.length > 0 ? front.description : firstParagraph(text),
168
+ whenToUse: front.whenToUse,
169
+ userInvocable: front.userInvocable,
170
+ category: category[0],
171
+ categoryLabel: category[1],
172
+ color: CATEGORY_COLORS[category[0]] ?? CATEGORY_COLORS.other,
173
+ files: stats.files,
174
+ bytes: stats.bytes,
175
+ human: humanSize(stats.bytes),
176
+ updatedAt: stats.updatedAt > 0 ? new Date(stats.updatedAt).toISOString() : null,
177
+ references: stats.references === 1,
178
+ assets: stats.assets === 1,
179
+ path: skillDir,
180
+ skillFile,
181
+ });
182
+ }
183
+ return skills;
184
+ }
185
+
186
+ /* ------------------------------------------------------------------ */
187
+ /* session log scanning (concatenated zstd frames with JSONL events) */
188
+ /* ------------------------------------------------------------------ */
189
+
190
+ function decodeSessionLog(buffer) {
191
+ const positions = [];
192
+ let from = 0;
193
+ while (true) {
194
+ const index = buffer.indexOf(ZSTD_MAGIC, from);
195
+ if (index < 0) break;
196
+ positions.push(index);
197
+ from = index + 1;
198
+ }
199
+ if (positions.length === 0) return "";
200
+ const chunks = [];
201
+ for (let index = 0; index < positions.length; index += 1) {
202
+ const start = positions[index];
203
+ const end = positions[index + 1] ?? buffer.length;
204
+ try {
205
+ chunks.push(zlib.zstdDecompressSync(buffer.subarray(start, end)));
206
+ } catch {
207
+ /* a false magic match inside compressed data */
208
+ }
209
+ }
210
+ return Buffer.concat(chunks).toString("utf8");
211
+ }
212
+
213
+ function walkSessionFiles(root) {
214
+ const files = [];
215
+ const walk = (dir, depth) => {
216
+ if (depth > 3) return;
217
+ let entries = [];
218
+ try {
219
+ entries = fs.readdirSync(dir, { withFileTypes: true });
220
+ } catch {
221
+ return;
222
+ }
223
+ for (const entry of entries) {
224
+ const full = path.join(dir, entry.name);
225
+ if (entry.isDirectory()) walk(full, depth + 1);
226
+ else if (entry.name.endsWith(".zstd")) files.push(full);
227
+ }
228
+ };
229
+ walk(root, 0);
230
+ return files;
231
+ }
232
+
233
+ function sessionSignature(home) {
234
+ const sessionDir = path.join(home, "sessions");
235
+ const files = walkSessionFiles(sessionDir);
236
+ const parts = [];
237
+ for (const file of files) {
238
+ try {
239
+ const stat = fs.statSync(file);
240
+ parts.push(`${path.basename(path.dirname(file))}:${stat.size}:${Math.floor(stat.mtimeMs)}`);
241
+ } catch {
242
+ /* ignore */
243
+ }
244
+ }
245
+ const skillsDir = path.join(home, "skills");
246
+ let skillsStamp = "";
247
+ try {
248
+ const stats = fs.readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
249
+ try {
250
+ return `${entry.name}:${fs.statSync(path.join(skillsDir, entry.name)).mtimeMs}`;
251
+ } catch {
252
+ return entry.name;
253
+ }
254
+ });
255
+ skillsStamp = stats.sort().join("|");
256
+ } catch {
257
+ skillsStamp = "";
258
+ }
259
+ return { files, key: `${parts.sort().join(";")}::${skillsStamp}` };
260
+ }
261
+
262
+ function collectUsage(files) {
263
+ const usage = new Map();
264
+ const sessionsById = new Map();
265
+ const dayKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
266
+ for (const file of files) {
267
+ let text = "";
268
+ try {
269
+ text = decodeSessionLog(fs.readFileSync(file));
270
+ } catch {
271
+ continue;
272
+ }
273
+ let sessionId = path.basename(path.dirname(file));
274
+ let origin = "user";
275
+ let cwd = "";
276
+ for (const line of text.split(/\r?\n/u)) {
277
+ if (line.length === 0) continue;
278
+ let event;
279
+ try {
280
+ event = JSON.parse(line);
281
+ } catch {
282
+ continue;
283
+ }
284
+ if (event.type === "session") {
285
+ sessionId = event.id ?? sessionId;
286
+ origin = event.origin ?? "user";
287
+ cwd = event.cwd ?? "";
288
+ sessionsById.set(sessionId, { sessionId, origin, cwd, createdAt: event.createdAt ?? null });
289
+ continue;
290
+ }
291
+ if (event.type !== "tool/call" || event.data?.name !== "skill") continue;
292
+ let args;
293
+ try {
294
+ args = typeof event.data.arguments === "string" ? JSON.parse(event.data.arguments) : event.data.arguments;
295
+ } catch {
296
+ args = undefined;
297
+ }
298
+ const name = typeof args?.name === "string" ? args.name : undefined;
299
+ if (name === undefined || name.length === 0) continue;
300
+ const at = Number(event.time) || null;
301
+ const record = usage.get(name) ?? { name, count: 0, lastAt: 0, days: new Map(), sessions: new Set() };
302
+ record.count += 1;
303
+ if (at !== null && at > record.lastAt) record.lastAt = at;
304
+ if (at !== null) {
305
+ const key = dayKey(at);
306
+ record.days.set(key, (record.days.get(key) ?? 0) + 1);
307
+ }
308
+ record.sessions.add(sessionId);
309
+ usage.set(name, record);
310
+ }
311
+ }
312
+ return { usage, sessionsById };
313
+ }
314
+
315
+ export function collectSkillsData(home = skillsHome(), sessionId = null) {
316
+ const { files, key } = sessionSignature(home);
317
+ const cacheKey = `${key}::${sessionId ?? ""}`;
318
+ const now = Date.now();
319
+ if (cache.data !== null && cache.key === cacheKey && now - cache.at < CACHE_TTL_MS) return cache.data;
320
+ const skills = listSkills(home);
321
+ const { usage } = collectUsage(files);
322
+ const today = new Date();
323
+ const days = [];
324
+ for (let index = 13; index >= 0; index -= 1) {
325
+ const date = new Date(today);
326
+ date.setDate(date.getDate() - index);
327
+ days.push(date.toISOString().slice(0, 10));
328
+ }
329
+ let totalRuns = 0;
330
+ const active = [];
331
+ for (const skill of skills) {
332
+ const record = usage.get(skill.name);
333
+ const series = days.map((day) => record?.days.get(day) ?? 0);
334
+ skill.usage = {
335
+ count: record?.count ?? 0,
336
+ lastAt: record?.lastAt ? new Date(record.lastAt).toISOString() : null,
337
+ sessions: record?.sessions.size ?? 0,
338
+ series,
339
+ week: series.slice(-7).reduce((sum, value) => sum + value, 0),
340
+ inCurrentSession: sessionId !== null && (record?.sessions.has(sessionId) ?? false),
341
+ };
342
+ totalRuns += skill.usage.count;
343
+ if (sessionId !== null && (record?.sessions.has(sessionId) ?? false)) {
344
+ active.push({ name: skill.name, lastAt: skill.usage.lastAt, count: record.count });
345
+ }
346
+ }
347
+ active.sort((left, right) => (right.lastAt ?? "").localeCompare(left.lastAt ?? ""));
348
+ const data = {
349
+ skills,
350
+ totals: {
351
+ skills: skills.length,
352
+ used: skills.filter((skill) => skill.usage.count > 0).length,
353
+ runs: totalRuns,
354
+ active: active.length,
355
+ },
356
+ active,
357
+ categories: Object.entries(CATEGORY_COLORS).map(([id, color]) => ({ id, label: (skills.find((skill) => skill.category === id)?.categoryLabel) ?? id, color })),
358
+ generatedAt: new Date().toISOString(),
359
+ days,
360
+ };
361
+ cache = { key: cacheKey, at: now, data };
362
+ return data;
363
+ }
364
+
365
+ export function readSkillContent(home, name) {
366
+ const skills = listSkills(home);
367
+ const skill = skills.find((item) => item.name === name || item.directory === name);
368
+ if (skill === undefined) return null;
369
+ let content = "";
370
+ try {
371
+ content = fs.readFileSync(skill.skillFile, "utf8");
372
+ } catch {
373
+ content = "";
374
+ }
375
+ return { ...skill, content: content.slice(0, 20000) };
376
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "dsh-gkb-ru-ui",
3
+ "version": "1.8.1",
4
+ "description": "Russian language pack for the DSH web GUI + sidebar polish (workspaces first, tools at bottom, collapsible workspace groups) + resilient AURORA skill/balance/spend tiles + beautiful skills catalog with live usage statistics.",
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
+ }
@@ -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-уведомления не отправляются.